// Copyright (C) 2021 The Qt Company Ltd.
// Copyright (C) 2022 Intel Corporation.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only

#include <QTest>
#include <QTestEventLoop>
#include <QSignalSpy>

#include <QtCore/QProcess>
#include <QtCore/QDir>
#include <QtCore/QElapsedTimer>
#include <QtCore/QFile>
#include <QtCore/QThread>
#include <QtCore/QTemporaryDir>
#include <QtCore/QRegularExpression>
#include <QtCore/QDebug>
#include <QtCore/QMetaType>
#include <QtCore/QScopeGuard>
#include <QtCore/qoperatingsystemversion.h>
#include <QtNetwork/QHostInfo>

#include <qplatformdefs.h>
#ifdef Q_OS_UNIX
#  include <private/qcore_unix_p.h>
#  include <sys/resource.h>
#  include <sys/wait.h>
#endif
#ifdef Q_OS_WIN
#  define WIN32_LEAN_AND_MEAN
#  include <windows.h>
#  include <winerror.h>
#endif

#include <QtTest/private/qemulationdetector_p.h>

#include <stdlib.h>

#include "crasher.h"

using namespace Qt::StringLiterals;

typedef void (QProcess::*QProcessErrorSignal)(QProcess::ProcessError);

class tst_QProcess : public QObject
{
    Q_OBJECT

public slots:
    void initTestCase();
    void cleanupTestCase();
    void init();

private slots:
    void getSetCheck();
    void constructing();
    void simpleStart();
    void startCommand();
    void startCommandEmptyString();
    void startWithOpen();
    void startWithOldOpen();
    void execute();
    void startDetached();
    void simpleStartFail_data();
    void simpleStartFail();
    void crashTest();
    void crashTest2();
    void echoTest_data();
    void echoTest();
    void echoTest2();
#ifdef Q_OS_WIN
    void echoTestGui();
    void testSetNamedPipeHandleState();
    void batFiles_data();
    void batFiles();
#endif
    void loopBackTest();
    void readTimeoutAndThenCrash();
    void deadWhileReading();
    void restartProcessDeadlock();
    void closeWriteChannel();
    void closeReadChannel();
    void openModes();
    void emitReadyReadOnlyWhenNewDataArrives();
    void softExitInSlots_data();
    void softExitInSlots();
    void mergedChannels();
    void forwardedChannels_data();
    void forwardedChannels();
    void atEnd();
    void atEnd2();
    void waitForFinishedWithTimeout();
    void waitForReadyReadInAReadyReadSlot();
    void waitForBytesWrittenInABytesWrittenSlot();
    void setEnvironment_data();
    void setEnvironment();
    void setProcessEnvironment_data();
    void setProcessEnvironment();
    void environmentIsSorted();
    void spaceInName();
    void setStandardInputFile();
    void setStandardInputFileFailure();
    void setStandardOutputFile_data();
    void setStandardOutputFile();
    void setStandardOutputFileFailure_data() { setStandardOutputFile_data(); }
    void setStandardOutputFileFailure();
    void setStandardOutputFileNullDevice();
    void setStandardOutputFileAndWaitForBytesWritten();
    void setStandardOutputProcess_data();
    void setStandardOutputProcess();
    void removeFileWhileProcessIsRunning();
    void fileWriterProcess();
    void switchReadChannels();
    void discardUnwantedOutput();
    void setWorkingDirectory();
    void setNonExistentWorkingDirectory_data();
    void setNonExistentWorkingDirectory();

    void exitStatus_data();
    void exitStatus();
    void waitForFinished();
    void hardExit();
    void softExit();
    void processInAThread();
    void processesInMultipleThreads();
    void spaceArgsTest_data();
    void spaceArgsTest();
#if defined(Q_OS_WIN)
    void nativeArguments();
    void createProcessArgumentsModifier();
#endif // Q_OS_WIN
#if defined(Q_OS_UNIX)
    void setChildProcessModifier_data();
    void setChildProcessModifier();
    void failChildProcessModifier_data() { setChildProcessModifier_data(); }
    void failChildProcessModifier();
    void throwInChildProcessModifier();
    void terminateInChildProcessModifier_data();
    void terminateInChildProcessModifier();
    void raiseInChildProcessModifier();
    void unixProcessParameters_data();
    void unixProcessParameters();
    void impossibleUnixProcessParameters_data();
    void impossibleUnixProcessParameters();
    void unixProcessParametersAndChildModifier();
    void unixProcessParametersOtherFileDescriptors();
#endif
    void exitCodeTest();
    void systemEnvironment();
    void lockupsInStartDetached();
    void waitForReadyReadForNonexistantProcess();
    void detachedProcessParameters_data();
    void detachedProcessParameters();
    void startFinishStartFinish();
    void invalidProgramString_data();
    void invalidProgramString();
    void onlyOneStartedSignal();
    void finishProcessBeforeReadingDone();
    void waitForStartedWithoutStart();
    void startStopStartStop();
    void startStopStartStopBuffers_data();
    void startStopStartStopBuffers();
    void processEventsInAReadyReadSlot_data();
    void processEventsInAReadyReadSlot();
    void startFromCurrentWorkingDir_data();
    void startFromCurrentWorkingDir();
    void syscallsAreRestartedInParent_data();
    void syscallsAreRestartedInParent();

    // keep these at the end, since they use lots of processes and sometimes
    // caused obscure failures to occur in tests that followed them (esp. on the Mac)
    void failToStart();
    void failToStartWithWait();
    void failToStartWithEventLoop();
    void failToStartEmptyArgs_data();
    void failToStartEmptyArgs();

protected slots:
    void readFromProcess();
    void exitLoopSlot();
    void processApplicationEvents();
    void restartProcess();
    void waitForReadyReadInAReadyReadSlotSlot();
    void waitForBytesWrittenInABytesWrittenSlotSlot();

private:
    QString nonExistentFileName = u"/this/file/cant/exist/hopefully"_s;

    qint64 bytesAvailable;
    QTemporaryDir m_temporaryDir;
    bool haveWorkingVFork = false;
};

void tst_QProcess::initTestCase()
{
#if defined(QT_ASAN_ENABLED)
    QSKIP("Skipping QProcess tests under ASAN as they are flaky (QTBUG-109329)");
#endif
    QVERIFY2(m_temporaryDir.isValid(), qPrintable(m_temporaryDir.errorString()));
    // chdir to our testdata path and execute helper apps relative to that.
    QString testdata_dir = QFileInfo(QFINDTESTDATA("testProcessNormal")).absolutePath();
    QVERIFY2(QDir::setCurrent(testdata_dir), qPrintable("Could not chdir to " + testdata_dir));

#if defined(Q_OS_LINUX) && QT_CONFIG(forkfd_pidfd)
    // see detect_clone_pidfd_support() in forkfd_linux.c for explanation
    waitid(/*P_PIDFD*/ idtype_t(3), INT_MAX, NULL, WEXITED|WNOHANG);
    haveWorkingVFork = (errno == EBADF);
#endif
}

void tst_QProcess::cleanupTestCase()
{
}

void tst_QProcess::init()
{
    bytesAvailable = 0;
}

// Testing get/set functions
void tst_QProcess::getSetCheck()
{
    QProcess obj1;
    // ProcessChannelMode QProcess::readChannelMode()
    // void QProcess::setProcessChannelMode(ProcessChannelMode)
    obj1.setProcessChannelMode(QProcess::ProcessChannelMode(QProcess::SeparateChannels));
    QCOMPARE(QProcess::ProcessChannelMode(QProcess::SeparateChannels), obj1.processChannelMode());
    obj1.setProcessChannelMode(QProcess::ProcessChannelMode(QProcess::MergedChannels));
    QCOMPARE(QProcess::ProcessChannelMode(QProcess::MergedChannels), obj1.processChannelMode());
    obj1.setProcessChannelMode(QProcess::ProcessChannelMode(QProcess::ForwardedChannels));
    QCOMPARE(QProcess::ProcessChannelMode(QProcess::ForwardedChannels), obj1.processChannelMode());

    // ProcessChannel QProcess::readChannel()
    // void QProcess::setReadChannel(ProcessChannel)
    obj1.setReadChannel(QProcess::ProcessChannel(QProcess::StandardOutput));
    QCOMPARE(QProcess::ProcessChannel(QProcess::StandardOutput), obj1.readChannel());
    obj1.setReadChannel(QProcess::ProcessChannel(QProcess::StandardError));
    QCOMPARE(QProcess::ProcessChannel(QProcess::StandardError), obj1.readChannel());
}

void tst_QProcess::constructing()
{
    QProcess process;
    QCOMPARE(process.readChannel(), QProcess::StandardOutput);
    QCOMPARE(process.workingDirectory(), QString());
    QCOMPARE(process.environment(), QStringList());
    QCOMPARE(process.error(), QProcess::UnknownError);
    QCOMPARE(process.state(), QProcess::NotRunning);
    QCOMPARE(process.processId(), 0);
    QCOMPARE(process.readAllStandardOutput(), QByteArray());
    QCOMPARE(process.readAllStandardError(), QByteArray());
    QCOMPARE(process.canReadLine(), false);

    // QIODevice
    QCOMPARE(process.openMode(), QIODevice::NotOpen);
    QVERIFY(!process.isOpen());
    QVERIFY(!process.isReadable());
    QVERIFY(!process.isWritable());
    QVERIFY(process.isSequential());
    QCOMPARE(process.pos(), qlonglong(0));
    QCOMPARE(process.size(), qlonglong(0));
    QVERIFY(process.atEnd());
    QCOMPARE(process.bytesAvailable(), qlonglong(0));
    QCOMPARE(process.bytesToWrite(), qlonglong(0));
    QVERIFY(!process.errorString().isEmpty());

    char c;
    QCOMPARE(process.read(&c, 1), qlonglong(-1));
    QCOMPARE(process.write(&c, 1), qlonglong(-1));

    QProcess proc2;
}

void tst_QProcess::simpleStart()
{
    qRegisterMetaType<QProcess::ProcessState>("QProcess::ProcessState");

    QScopedPointer<QProcess> process(new QProcess);
    QSignalSpy spy(process.data(), &QProcess::stateChanged);
    QVERIFY(spy.isValid());
    connect(process.data(), &QIODevice::readyRead, this, &tst_QProcess::readFromProcess);

    /* valgrind dislike SUID binaries(those that have the `s'-flag set), which
     * makes it fail to start the process. For this reason utilities like `ping' won't
     * start, when the auto test is run through `valgrind'. */
    process->start("testProcessNormal/testProcessNormal");
    if (process->state() != QProcess::Starting)
        QCOMPARE(process->state(), QProcess::Running);
    QVERIFY2(process->waitForStarted(5000), qPrintable(process->errorString()));
    QCOMPARE(process->state(), QProcess::Running);
    QTRY_COMPARE(process->state(), QProcess::NotRunning);

    process.reset();

    QCOMPARE(spy.size(), 3);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(0).at(0)), QProcess::Starting);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(1).at(0)), QProcess::Running);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(spy.at(2).at(0)), QProcess::NotRunning);
}

void tst_QProcess::startCommand()
{
    QProcess process;
    process.startCommand("testProcessSpacesArgs/nospace foo \"b a r\" baz");
    QVERIFY2(process.waitForStarted(), qPrintable(process.errorString()));
    QVERIFY2(process.waitForFinished(), qPrintable(process.errorString()));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
    QByteArray actual = process.readAll();
    actual.remove(0, actual.indexOf('|') + 1);
    QByteArray expected = "foo|b a r|baz";
    QCOMPARE(actual, expected);
}

void tst_QProcess::startCommandEmptyString()
{
    static const char warningMsg[] =
            "QProcess::startCommand: empty or whitespace-only command was provided";
    QProcess process;

    QTest::ignoreMessage(QtWarningMsg, warningMsg);
    process.startCommand("");
    QVERIFY(!process.waitForStarted());

    QTest::ignoreMessage(QtWarningMsg, warningMsg);
    process.startCommand("   ");
    QVERIFY(!process.waitForStarted());

    QTest::ignoreMessage(QtWarningMsg, warningMsg);
    process.startCommand("\t\n");
    QVERIFY(!process.waitForStarted());
}

void tst_QProcess::startWithOpen()
{
    QProcess p;
    QTest::ignoreMessage(QtWarningMsg, "QProcess::start: program not set");
    QCOMPARE(p.open(QIODevice::ReadOnly), false);

    p.setProgram("testProcessNormal/testProcessNormal");
    QCOMPARE(p.program(), QString("testProcessNormal/testProcessNormal"));

    p.setArguments(QStringList() << "arg1" << "arg2");
    QCOMPARE(p.arguments().size(), 2);

    QVERIFY(p.open(QIODevice::ReadOnly));
    QCOMPARE(p.openMode(), QIODevice::ReadOnly);
    QVERIFY(p.waitForFinished(5000));
}

void tst_QProcess::startWithOldOpen()
{
    // similar to the above, but we start with start() actually
    // while open() is overridden to call QIODevice::open().
    // This tests the BC requirement that "it works with the old implementation"
    class OverriddenOpen : public QProcess
    {
    public:
        virtual bool open(OpenMode mode) override
        { return QIODevice::open(mode); }
    };

    OverriddenOpen p;
    p.start("testProcessNormal/testProcessNormal");
    QVERIFY(p.waitForStarted(5000));
    QVERIFY(p.waitForFinished(5000));
}

void tst_QProcess::execute()
{
    QCOMPARE(QProcess::execute("testProcessNormal/testProcessNormal",
                               QStringList() << "arg1" << "arg2"), 0);
    QCOMPARE(QProcess::execute("nonexistingexe"), -2);
}

void tst_QProcess::startDetached()
{
    QVERIFY(QProcess::startDetached("testProcessNormal/testProcessNormal",
                                    QStringList() << "arg1" << "arg2"));
    QCOMPARE(QProcess::startDetached("nonexistingexe"), false);
}

void tst_QProcess::simpleStartFail_data()
{
    QTest::addColumn<bool>("detached");
    QTest::addColumn<bool>("unixCloseFileDescriptors");

    QTest::addRow("normal") << false << false;
    QTest::addRow("detached") << true << false;

#ifdef Q_OS_UNIX
    // make sure UnixProcessFlag::CloseFileDescriptors doesn't affect our
    // error reporting
    QTest::addRow("normal+closefds") << false << true;
    QTest::addRow("detached+closefds") << true << true;
#endif
}

void tst_QProcess::simpleStartFail()
{
    // for more complex and stressful cases, see the other failToStart* tests
    QFETCH(bool, detached);
    QProcess process;

#ifdef Q_OS_UNIX
    QFETCH(bool, unixCloseFileDescriptors);
    if (unixCloseFileDescriptors)
        process.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    process.setProgram(nonExistentFileName);

    if (detached) {
        qint64 pid = -1;
        QVERIFY(!process.startDetached(&pid));
        QCOMPARE(pid, -1);
    } else {
        process.start();
        QVERIFY(!process.waitForFinished());

        QCOMPARE(stateSpy.size(), 2);
        QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
        QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning);
    }

    QCOMPARE(errorOccurredSpy.size(), 1);
    QCOMPARE(process.error(), QProcess::FailedToStart);
    QCOMPARE_NE(process.errorString(), "Unknown error");

#ifdef Q_OS_UNIX
    QVERIFY2(process.errorString().contains(": execve: "), process.errorString().toLocal8Bit());
    int errorcode = ENOENT;
#else
    // value happens to match ENOENT, but that's a coincidence
    int errorcode = ERROR_FILE_NOT_FOUND;
#endif
    QVERIFY2(process.errorString().contains(qt_error_string(errorcode)),
             process.errorString().toLocal8Bit());
}

void tst_QProcess::readFromProcess()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    QVERIFY(process);
    while (process->canReadLine()) {
        process->readLine();
    }
}

void tst_QProcess::crashTest()
{
    qRegisterMetaType<QProcess::ProcessState>("QProcess::ProcessState");
    QScopedPointer<QProcess> process(new QProcess);
    QSignalSpy stateSpy(process.data(), &QProcess::stateChanged);
    QVERIFY(stateSpy.isValid());
    process->start("testProcessCrash/testProcessCrash");
    QVERIFY(process->waitForStarted(5000));

    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");

    QSignalSpy spy(process.data(), &QProcess::errorOccurred);
    QSignalSpy spy2(process.data(), &QProcess::finished);
    QVERIFY(spy.isValid());
    QVERIFY(spy2.isValid());

    QVERIFY(process->waitForFinished(30000));

    QCOMPARE(spy.size(), 1);
    QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed);

    QCOMPARE(spy2.size(), 1);
    QCOMPARE(*static_cast<const QProcess::ExitStatus *>(spy2.at(0).at(1).constData()), QProcess::CrashExit);

    QCOMPARE(process->exitStatus(), QProcess::CrashExit);

    // delete process;
    process.reset();

    QCOMPARE(stateSpy.size(), 3);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(0).at(0)), QProcess::Starting);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(1).at(0)), QProcess::Running);
    QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(2).at(0)), QProcess::NotRunning);
}

void tst_QProcess::crashTest2()
{
    QProcess process;
    process.start("testProcessCrash/testProcessCrash");
    QVERIFY(process.waitForStarted(5000));

    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");

    QSignalSpy spy(&process, static_cast<QProcessErrorSignal>(&QProcess::errorOccurred));
    QSignalSpy spy2(&process, &QProcess::finished);

    QVERIFY(spy.isValid());
    QVERIFY(spy2.isValid());

    QObject::connect(&process, &QProcess::finished, this, &tst_QProcess::exitLoopSlot);

    QTestEventLoop::instance().enterLoop(30);
    if (QTestEventLoop::instance().timeout())
        QFAIL("Failed to detect crash : operation timed out");

    QCOMPARE(spy.size(), 1);
    QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed);

    QCOMPARE(spy2.size(), 1);
    QCOMPARE(*static_cast<const QProcess::ExitStatus *>(spy2.at(0).at(1).constData()), QProcess::CrashExit);

    QCOMPARE(process.exitStatus(), QProcess::CrashExit);
}

void tst_QProcess::echoTest_data()
{
    QTest::addColumn<QByteArray>("input");

    QTest::newRow("1") << QByteArray("H");
    QTest::newRow("2") << QByteArray("He");
    QTest::newRow("3") << QByteArray("Hel");
    QTest::newRow("4") << QByteArray("Hell");
    QTest::newRow("5") << QByteArray("Hello");
    QTest::newRow("100 bytes") << QByteArray(100, '@');
    QTest::newRow("1000 bytes") << QByteArray(1000, '@');
    QTest::newRow("10000 bytes") << QByteArray(10000, '@');
}

void tst_QProcess::echoTest()
{
    QFETCH(QByteArray, input);

    QProcess process;
    connect(&process, &QIODevice::readyRead, this, &tst_QProcess::exitLoopSlot);

    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted(5000));

    process.write(input);

    QElapsedTimer stopWatch;
    stopWatch.start();
    do {
        QVERIFY(process.isOpen());
        QTestEventLoop::instance().enterLoop(2);
    } while (stopWatch.elapsed() < 60000 && process.bytesAvailable() < input.size());
    if (stopWatch.elapsed() >= 60000)
        QFAIL("Timed out");

    QByteArray message = process.readAll();
    QCOMPARE(message.size(), input.size());

    char *c1 = message.data();
    char *c2 = input.data();
    while (*c1 && *c2) {
        if (*c1 != *c2)
            QCOMPARE(*c1, *c2);
        ++c1;
        ++c2;
    }
    QCOMPARE(*c1, *c2);

    process.write("", 1);

    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::exitLoopSlot()
{
    QTestEventLoop::instance().exitLoop();
}

void tst_QProcess::processApplicationEvents()
{
    QCoreApplication::processEvents();
}

void tst_QProcess::echoTest2()
{

    QProcess process;
    connect(&process, &QIODevice::readyRead, this, &tst_QProcess::exitLoopSlot);

    process.start("testProcessEcho2/testProcessEcho2");
    QVERIFY(process.waitForStarted(5000));
    QVERIFY(!process.waitForReadyRead(250));
    QCOMPARE(process.error(), QProcess::Timedout);

    process.write("Hello");
    QSignalSpy spy0(&process, &QProcess::channelReadyRead);
    QSignalSpy spy1(&process, &QProcess::readyReadStandardOutput);
    QSignalSpy spy2(&process, &QProcess::readyReadStandardError);

    QVERIFY(spy0.isValid());
    QVERIFY(spy1.isValid());
    QVERIFY(spy2.isValid());

    QElapsedTimer stopWatch;
    stopWatch.start();
    forever {
        QTestEventLoop::instance().enterLoop(1);
        if (stopWatch.elapsed() >= 30000)
            QFAIL("Timed out");
        process.setReadChannel(QProcess::StandardOutput);
        qint64 baso = process.bytesAvailable();

        process.setReadChannel(QProcess::StandardError);
        qint64 base = process.bytesAvailable();
        if (baso == 5 && base == 5)
            break;
    }

    QVERIFY(spy0.size() > 0);
    QVERIFY(spy1.size() > 0);
    QVERIFY(spy2.size() > 0);

    QCOMPARE(process.readAllStandardOutput(), QByteArray("Hello"));
    QCOMPARE(process.readAllStandardError(), QByteArray("Hello"));

    process.write("", 1);
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

#if defined(Q_OS_WIN)
void tst_QProcess::echoTestGui()
{
    QProcess process;

    process.start("testProcessEchoGui/testProcessEchoGui");


    process.write("Hello");
    process.write("q");

    QVERIFY(process.waitForFinished(50000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    QCOMPARE(process.readAllStandardOutput(), QByteArray("Hello"));
    QCOMPARE(process.readAllStandardError(), QByteArray("Hello"));
}

void tst_QProcess::testSetNamedPipeHandleState()
{
    QProcess process;
    process.setProcessChannelMode(QProcess::SeparateChannels);
    process.start("testSetNamedPipeHandleState/testSetNamedPipeHandleState");
    QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitCode(), 0);
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
}
#endif // Q_OS_WIN

#if defined(Q_OS_WIN)
void tst_QProcess::batFiles_data()
{
    QTest::addColumn<QString>("batFile");
    QTest::addColumn<QByteArray>("output");

    QTest::newRow("simple") << QFINDTESTDATA("testBatFiles/simple.bat") << QByteArray("Hello");
    QTest::newRow("with space") << QFINDTESTDATA("testBatFiles/with space.bat") << QByteArray("Hello");
}

void tst_QProcess::batFiles()
{
    QFETCH(QString, batFile);
    QFETCH(QByteArray, output);

    QProcess proc;

    proc.start(batFile, QStringList());

    QVERIFY(proc.waitForFinished(5000));
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 0);

    QVERIFY(proc.bytesAvailable() > 0);

    QVERIFY(proc.readAll().startsWith(output));
}
#endif // Q_OS_WIN

void tst_QProcess::exitStatus_data()
{
    QTest::addColumn<QStringList>("processList");
    QTest::addColumn<QList<QProcess::ExitStatus> >("exitStatus");

    QTest::newRow("normal") << (QStringList() << "testProcessNormal/testProcessNormal")
                            << (QList<QProcess::ExitStatus>() << QProcess::NormalExit);
    QTest::newRow("crash") << (QStringList() << "testProcessCrash/testProcessCrash")
                            << (QList<QProcess::ExitStatus>() << QProcess::CrashExit);

    QTest::newRow("normal-crash") << (QStringList()
                                      << "testProcessNormal/testProcessNormal"
                                      << "testProcessCrash/testProcessCrash")
                                  << (QList<QProcess::ExitStatus>()
                                      << QProcess::NormalExit
                                      << QProcess::CrashExit);
    QTest::newRow("crash-normal") << (QStringList()
                                      << "testProcessCrash/testProcessCrash"
                                      << "testProcessNormal/testProcessNormal")
                                  << (QList<QProcess::ExitStatus>()
                                      << QProcess::CrashExit
                                      << QProcess::NormalExit);
}

void tst_QProcess::exitStatus()
{
    QProcess process;
    QFETCH(QStringList, processList);
    QFETCH(QList<QProcess::ExitStatus>, exitStatus);

    QCOMPARE(exitStatus.size(), processList.size());
    for (int i = 0; i < processList.size(); ++i) {
        process.start(processList.at(i));
        QVERIFY(process.waitForStarted(5000));
        QVERIFY(process.waitForFinished(30000));

        QCOMPARE(process.exitStatus(), exitStatus.at(i));
    }
}

void tst_QProcess::loopBackTest()
{

    QProcess process;
    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted(5000));

    for (int i = 0; i < 100; ++i) {
        process.write("Hello");
        do {
            QVERIFY(process.waitForReadyRead(5000));
        } while (process.bytesAvailable() < 5);
        QCOMPARE(process.readAll(), QByteArray("Hello"));
    }

    process.write("", 1);
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::readTimeoutAndThenCrash()
{

    QProcess process;
    process.start("testProcessEcho/testProcessEcho");
    if (process.state() != QProcess::Starting)
        QCOMPARE(process.state(), QProcess::Running);

    QVERIFY(process.waitForStarted(5000));
    QCOMPARE(process.state(), QProcess::Running);

    QVERIFY(!process.waitForReadyRead(5000));
    QCOMPARE(process.error(), QProcess::Timedout);

    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    QSignalSpy spy(&process, &QProcess::errorOccurred);
    QVERIFY(spy.isValid());

    process.kill();

    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.state(), QProcess::NotRunning);

    QCOMPARE(spy.size(), 1);
    QCOMPARE(*static_cast<const QProcess::ProcessError *>(spy.at(0).at(0).constData()), QProcess::Crashed);
}

void tst_QProcess::waitForFinished()
{
    QProcess process;

    process.start("testProcessOutput/testProcessOutput");

    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);

    QString output = process.readAll();
    QCOMPARE(output.count("\n"), 10*1024);

    process.start("blurdybloop");
    QVERIFY(!process.waitForFinished());
    QCOMPARE(process.error(), QProcess::FailedToStart);
}

void tst_QProcess::deadWhileReading()
{
    QProcess process;

    process.start("testProcessDeadWhileReading/testProcessDeadWhileReading");

    QString output;

    QVERIFY(process.waitForStarted(5000));
    while (process.waitForReadyRead(5000))
        output += process.readAll();

    QCOMPARE(output.count("\n"), 10*1024);
    process.waitForFinished();
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::restartProcessDeadlock()
{
    // The purpose of this test is to detect whether restarting a
    // process in the finished() connected slot causes a deadlock
    // because of the way QProcessManager uses its locks.
    QProcess process;
    connect(&process, &QProcess::finished, this, &tst_QProcess::restartProcess);

    process.start("testProcessEcho/testProcessEcho");

    QCOMPARE(process.write("", 1), qlonglong(1));
    QVERIFY(process.waitForFinished(5000));

    QObject::disconnect(&process, &QProcess::finished, nullptr, nullptr);

    QCOMPARE(process.write("", 1), qlonglong(1));
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::restartProcess()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    QVERIFY(process);
    process->start("testProcessEcho/testProcessEcho");
}

void tst_QProcess::closeWriteChannel()
{
    QByteArray testData("Data to read");
    QProcess more;
    more.start("testProcessEOF/testProcessEOF");

    QVERIFY(more.waitForStarted(5000));
    QVERIFY(!more.waitForReadyRead(250));
    QCOMPARE(more.error(), QProcess::Timedout);

    QCOMPARE(more.write(testData), qint64(testData.size()));

    QVERIFY(!more.waitForReadyRead(250));
    QCOMPARE(more.error(), QProcess::Timedout);

    more.closeWriteChannel();
    // During closeWriteChannel() call, we might also get an I/O completion
    // on the read pipe. So, take this into account before waiting for
    // the new incoming data.
    while (more.bytesAvailable() < testData.size())
        QVERIFY(more.waitForReadyRead(5000));
    QCOMPARE(more.readAll(), testData);

    if (more.state() == QProcess::Running)
        QVERIFY(more.waitForFinished(5000));
    QCOMPARE(more.exitStatus(), QProcess::NormalExit);
    QCOMPARE(more.exitCode(), 0);
}

void tst_QProcess::closeReadChannel()
{
    for (int i = 0; i < 10; ++i) {
        QProcess::ProcessChannel channel1 = QProcess::StandardOutput;
        QProcess::ProcessChannel channel2 = QProcess::StandardError;

        QProcess proc;
        proc.start("testProcessEcho2/testProcessEcho2");
        QVERIFY(proc.waitForStarted(5000));
        proc.closeReadChannel(i&1 ? channel2 : channel1);
        proc.setReadChannel(i&1 ? channel2 : channel1);
        proc.write("Data");

        QVERIFY(!proc.waitForReadyRead(5000));
        QVERIFY(proc.readAll().isEmpty());

        proc.setReadChannel(i&1 ? channel1 : channel2);

        while (proc.bytesAvailable() < 4 && proc.waitForReadyRead(5000))
        { }

        QCOMPARE(proc.readAll(), QByteArray("Data"));

        proc.write("", 1);
        QVERIFY(proc.waitForFinished(5000));
        QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
        QCOMPARE(proc.exitCode(), 0);
    }
}

void tst_QProcess::openModes()
{
    QProcess proc;
    QVERIFY(!proc.isOpen());
    QCOMPARE(proc.openMode(), QProcess::NotOpen);
    proc.start("testProcessEcho3/testProcessEcho3");
    QVERIFY(proc.waitForStarted(5000));
    QVERIFY(proc.isOpen());
    QCOMPARE(proc.openMode(), QProcess::ReadWrite);
    QVERIFY(proc.isReadable());
    QVERIFY(proc.isWritable());

    proc.write("Data");

    proc.closeWriteChannel();

    QVERIFY(proc.isWritable());
    QCOMPARE(proc.openMode(), QProcess::ReadWrite);

    while (proc.bytesAvailable() < 4 && proc.waitForReadyRead(5000))
    { }

    QCOMPARE(proc.readAll().constData(), QByteArray("Data").constData());

    proc.closeReadChannel(QProcess::StandardOutput);

    QCOMPARE(proc.openMode(), QProcess::ReadWrite);
    QVERIFY(proc.isReadable());

    proc.closeReadChannel(QProcess::StandardError);

    QCOMPARE(proc.openMode(), QProcess::ReadWrite);
    QVERIFY(proc.isReadable());

    proc.close();
    QVERIFY(!proc.isOpen());
    QVERIFY(!proc.isReadable());
    QVERIFY(!proc.isWritable());
    QCOMPARE(proc.state(), QProcess::NotRunning);
}

void tst_QProcess::emitReadyReadOnlyWhenNewDataArrives()
{

    QProcess proc;
    connect(&proc, &QIODevice::readyRead, this, &tst_QProcess::exitLoopSlot);
    QSignalSpy spy(&proc, &QProcess::readyRead);
    QVERIFY(spy.isValid());

    proc.start("testProcessEcho/testProcessEcho");

    QCOMPARE(spy.size(), 0);

    proc.write("A");

    QTestEventLoop::instance().enterLoop(5);
    if (QTestEventLoop::instance().timeout())
        QFAIL("Operation timed out");

    QCOMPARE(spy.size(), 1);

    QTestEventLoop::instance().enterLoop(1);
    QVERIFY(QTestEventLoop::instance().timeout());
    QVERIFY(!proc.waitForReadyRead(250));

    QObject::disconnect(&proc, &QIODevice::readyRead, nullptr, nullptr);
    proc.write("B");
    QVERIFY(proc.waitForReadyRead(5000));

    proc.write("", 1);
    QVERIFY(proc.waitForFinished(5000));
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 0);
}

void tst_QProcess::hardExit()
{
    QProcess proc;

    proc.start("testProcessEcho/testProcessEcho");

    QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString()));

#if defined(Q_OS_QNX)
    // QNX may lose the kill if it's delivered while the forked process
    // is doing the exec that morphs it into testProcessEcho.  It's very
    // unlikely that a normal application would do such a thing.  Make
    // sure the test doesn't accidentally try to do it.
    proc.write("A");
    QVERIFY(proc.waitForReadyRead(5000));
#endif

    proc.kill();

    QVERIFY(proc.waitForFinished(5000));
    QCOMPARE(int(proc.state()), int(QProcess::NotRunning));
    QCOMPARE(int(proc.error()), int(QProcess::Crashed));
}

void tst_QProcess::softExit()
{
    QProcess proc;
    QCOMPARE(proc.processId(), 0);
    proc.start("testSoftExit/testSoftExit");

    QVERIFY(proc.waitForStarted(10000));
    QVERIFY(proc.waitForReadyRead(10000));

    QVERIFY(proc.processId() > 0);

    proc.terminate();

    QVERIFY(proc.waitForFinished(10000));
    QCOMPARE(int(proc.state()), int(QProcess::NotRunning));
    QCOMPARE(int(proc.error()), int(QProcess::UnknownError));
}

class SoftExitProcess : public QProcess
{
    Q_OBJECT
public:
    bool waitedForFinished;

    SoftExitProcess(int n) : waitedForFinished(false), n(n), killing(false)
    {
        connect(this, &QProcess::finished, this, &SoftExitProcess::finishedSlot);

        switch (n) {
        case 0:
            setProcessChannelMode(QProcess::MergedChannels);
            connect(this, &QIODevice::readyRead, this, &SoftExitProcess::terminateSlot);
            break;
        case 1:
            connect(this, &QProcess::readyReadStandardOutput,
                    this, &SoftExitProcess::terminateSlot);
            break;
        case 2:
            connect(this, &QProcess::readyReadStandardError,
                    this, &SoftExitProcess::terminateSlot);
            break;
        case 3:
            connect(this, &QProcess::started,
                    this, &SoftExitProcess::terminateSlot);
            break;
        case 4:
            setProcessChannelMode(QProcess::MergedChannels);
            connect(this, SIGNAL(channelReadyRead(int)), this, SLOT(terminateSlot()));
            break;
        default:
            connect(this, &QProcess::stateChanged,
                    this, &SoftExitProcess::terminateSlot);
            break;
        }
    }

    void writeAfterStart(const char *buf, int count)
    {
        dataToWrite = QByteArray(buf, count);
    }

    void start(const QString &program)
    {
        QProcess::start(program);
        writePendingData();
    }

public slots:
    void terminateSlot()
    {
        writePendingData(); // In cases 3 and 5 we haven't written the data yet.
        if (killing || (n == 5 && state() != Running)) {
            // Don't try to kill the process before it is running - that can
            // be hazardous, as the actual child process might not be running
            // yet. Also, don't kill it "recursively".
            return;
        }
        killing = true;
        readAll();
        terminate();
        if ((waitedForFinished = waitForFinished(5000)) == false) {
            kill();
            if (state() != NotRunning)
                waitedForFinished = waitForFinished(5000);
        }
    }

    void finishedSlot(int, QProcess::ExitStatus)
    {
        waitedForFinished = true;
    }

private:
    void writePendingData()
    {
        if (!dataToWrite.isEmpty()) {
            write(dataToWrite);
            dataToWrite.clear();
        }
    }

private:
    int n;
    bool killing;
    QByteArray dataToWrite;
};

void tst_QProcess::softExitInSlots_data()
{
    QTest::addColumn<QString>("appName");
    QTest::addColumn<int>("signalToConnect");

    QByteArray dataTagPrefix("gui app ");
#ifndef QT_NO_WIDGETS
    for (int i = 0; i < 6; ++i) {
        QTest::newRow(dataTagPrefix + QByteArray::number(i))
                << "testGuiProcess/testGuiProcess" << i;
    }
#endif

    dataTagPrefix = "console app ";
    for (int i = 0; i < 6; ++i) {
        QTest::newRow(dataTagPrefix + QByteArray::number(i))
                << "testProcessEcho2/testProcessEcho2" << i;
    }
}

void tst_QProcess::softExitInSlots()
{
    QFETCH(QString, appName);
    QFETCH(int, signalToConnect);

    SoftExitProcess proc(signalToConnect);
    proc.writeAfterStart("OLEBOLE", 8); // include the \0
    proc.start(appName);
    QTRY_VERIFY_WITH_TIMEOUT(proc.waitedForFinished, 60000);
    QCOMPARE(proc.state(), QProcess::NotRunning);
}

void tst_QProcess::mergedChannels()
{
    QProcess process;
    process.setProcessChannelMode(QProcess::MergedChannels);
    QCOMPARE(process.processChannelMode(), QProcess::MergedChannels);

    process.start("testProcessEcho2/testProcessEcho2");

    QVERIFY(process.waitForStarted(5000));

    {
        QCOMPARE(process.write("abc"), qlonglong(3));
        while (process.bytesAvailable() < 6)
            QVERIFY(process.waitForReadyRead(5000));
        QCOMPARE(process.readAllStandardOutput(), QByteArray("aabbcc"));
        QTest::ignoreMessage(QtWarningMsg,
            "QProcess::readAllStandardError: Called with MergedChannels");
        QCOMPARE(process.readAllStandardError(), QByteArray());
    }

    for (int i = 0; i < 100; ++i) {
        QCOMPARE(process.write("abc"), qlonglong(3));
        while (process.bytesAvailable() < 6)
            QVERIFY(process.waitForReadyRead(5000));
        QCOMPARE(process.readAll(), QByteArray("aabbcc"));
    }

    process.closeWriteChannel();
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::forwardedChannels_data()
{
    QTest::addColumn<bool>("detach");
    QTest::addColumn<int>("mode");
    QTest::addColumn<int>("inmode");
    QTest::addColumn<QByteArray>("outdata");
    QTest::addColumn<QByteArray>("errdata");

    QTest::newRow("separate")
            << false
            << int(QProcess::SeparateChannels) << int(QProcess::ManagedInputChannel)
            << QByteArray() << QByteArray();
    QTest::newRow("forwarded")
            << false
            << int(QProcess::ForwardedChannels) << int(QProcess::ManagedInputChannel)
            << QByteArray("forwarded") << QByteArray("forwarded");
    QTest::newRow("stdout")
            << false
            << int(QProcess::ForwardedOutputChannel) << int(QProcess::ManagedInputChannel)
            << QByteArray("forwarded") << QByteArray();
    QTest::newRow("stderr")
            << false
            << int(QProcess::ForwardedErrorChannel) << int(QProcess::ManagedInputChannel)
            << QByteArray() << QByteArray("forwarded");
    QTest::newRow("fwdinput")
            << false
            << int(QProcess::ForwardedErrorChannel) << int(QProcess::ForwardedInputChannel)
            << QByteArray() << QByteArray("input");
    QTest::newRow("detached-default-forwarding")
            << true
            << int(QProcess::SeparateChannels) << int(QProcess::ManagedInputChannel)
            << QByteArray("out data") << QByteArray("err data");
    QTest::newRow("detached-merged-forwarding")
            << true
            << int(QProcess::MergedChannels) << int(QProcess::ManagedInputChannel)
            << QByteArray("out data" "err data") << QByteArray();
}

void tst_QProcess::forwardedChannels()
{
    QFETCH(bool, detach);
    QFETCH(int, mode);
    QFETCH(int, inmode);
    QFETCH(QByteArray, outdata);
    QFETCH(QByteArray, errdata);

    QProcess process;
    process.start("testForwarding/testForwarding",
                  QStringList() << QString::number(mode) << QString::number(inmode)
                                << QString::number(bool(detach)));
    QVERIFY(process.waitForStarted(5000));
    QCOMPARE(process.write("input"), 5);
    process.closeWriteChannel();
    QVERIFY(process.waitForFinished(40000));    // testForwarding has a 30s wait
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    const char *err;
    switch (process.exitCode()) {
    case 0: err = "ok"; break;
    case 1: err = "processChannelMode is wrong"; break;
    case 11: err = "inputChannelMode is wrong"; break;
    case 2: err = "failed to start"; break;
    case 3: err = "failed to write"; break;
    case 4: err = "did not finish"; break;
    case 5: err = "unexpected stdout"; break;
    case 6: err = "unexpected stderr"; break;
    case 12: err = "cannot create temp file"; break;
    case 13: err = "startDetached failed"; break;
    case 14: err = "waitForDoneFileWritten timed out"; break;
    default: err = "unknown exit code"; break;
    }
    QVERIFY2(!process.exitCode(), err);
    QCOMPARE(process.readAllStandardOutput(), outdata);
    QCOMPARE(process.readAllStandardError(), errdata);
}

void tst_QProcess::atEnd()
{
    QProcess process;

    process.start("testProcessEcho/testProcessEcho");
    process.write("abcdefgh\n");

    while (process.bytesAvailable() < 8)
        QVERIFY(process.waitForReadyRead(5000));

    QTextStream stream(&process);
    QVERIFY(!stream.atEnd());
    QString tmp = stream.readLine();
    QVERIFY(stream.atEnd());
    QCOMPARE(tmp, QString::fromLatin1("abcdefgh"));

    process.write("", 1);
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

class TestThread : public QThread
{
    Q_OBJECT
public:
    inline int code()
    {
        return exitCode;
    }

protected:
    inline void run() override
    {
        exitCode = 90210;

        QProcess process;
        connect(&process, &QProcess::finished, this, &TestThread::catchExitCode, Qt::DirectConnection);

        process.start("testProcessEcho/testProcessEcho");

        QCOMPARE(process.write("abc\0", 4), qint64(4));
        exitCode = exec();
    }

protected slots:
    inline void catchExitCode(int exitCode)
    {
        this->exitCode = exitCode;
        exit(exitCode);
    }

private:
    int exitCode;
};

void tst_QProcess::processInAThread()
{
    for (int i = 0; i < 10; ++i) {
        TestThread thread;
        thread.start();
        QVERIFY(thread.wait(10000));
        QCOMPARE(thread.code(), 0);
    }
}

void tst_QProcess::processesInMultipleThreads()
{
    if (QTestPrivate::isRunningArmOnX86())
        QSKIP("Test is too slow to run on emulator");

#if defined(Q_OS_QNX)
    QSKIP("QNX: Large amount of threads is unstable and do not finish in given time");
#endif

    for (int i = 0; i < 10; ++i) {
        // run from 1 to 10 threads, but run at least some tests
        // with more threads than the ideal
        int threadCount = i;
        if (i > 7)
            threadCount = qMax(threadCount, QThread::idealThreadCount() + 2);

        QList<TestThread *> threads(threadCount);
        QScopeGuard cleanup([&threads]() { qDeleteAll(threads); });
        for (int j = 0; j < threadCount; ++j)
            threads[j] = new TestThread;
        for (int j = 0; j < threadCount; ++j)
            threads[j]->start();
        for (int j = 0; j < threadCount; ++j)
            QVERIFY(threads[j]->wait(10000));
        for (int j = 0; j < threadCount; ++j)
            QCOMPARE(threads[j]->code(), 0);
    }
}

void tst_QProcess::waitForFinishedWithTimeout()
{
    QProcess process;

    process.start("testProcessEcho/testProcessEcho");

    QVERIFY(process.waitForStarted(5000));
    QVERIFY(!process.waitForFinished(1));

    process.write("", 1);

    QVERIFY(process.waitForFinished());
}

void tst_QProcess::waitForReadyReadInAReadyReadSlot()
{
    QProcess process;
    connect(&process, &QIODevice::readyRead, this, &tst_QProcess::waitForReadyReadInAReadyReadSlotSlot);
    connect(&process, &QProcess::finished, this, &tst_QProcess::exitLoopSlot);
    bytesAvailable = 0;

    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted(5000));

    QSignalSpy spy(&process, &QProcess::readyRead);
    QVERIFY(spy.isValid());
    process.write("foo");
    QTestEventLoop::instance().enterLoop(30);
    QVERIFY(!QTestEventLoop::instance().timeout());

    QCOMPARE(spy.size(), 1);

    process.disconnect();
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
    QVERIFY(process.bytesAvailable() >= bytesAvailable);
}

void tst_QProcess::waitForReadyReadInAReadyReadSlotSlot()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    QVERIFY(process);
    bytesAvailable = process->bytesAvailable();
    process->write("bar", 4);
    QVERIFY(process->waitForReadyRead(5000));
    QVERIFY(process->bytesAvailable() > bytesAvailable);
    bytesAvailable = process->bytesAvailable();
    QTestEventLoop::instance().exitLoop();
}

void tst_QProcess::waitForBytesWrittenInABytesWrittenSlot()
{
    QProcess process;
    connect(&process, &QIODevice::bytesWritten, this, &tst_QProcess::waitForBytesWrittenInABytesWrittenSlotSlot);
    bytesAvailable = 0;

    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted(5000));

    QSignalSpy spy(&process, &QProcess::bytesWritten);
    QVERIFY(spy.isValid());
    process.write("f");
    QTestEventLoop::instance().enterLoop(30);
    QVERIFY(!QTestEventLoop::instance().timeout());

    QCOMPARE(spy.size(), 1);
    process.write("", 1);
    process.disconnect();
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::waitForBytesWrittenInABytesWrittenSlotSlot()
{
    QProcess *process = qobject_cast<QProcess *>(sender());
    QVERIFY(process);
    process->write("b");
    QVERIFY(process->waitForBytesWritten(5000));
    QTestEventLoop::instance().exitLoop();
}

void tst_QProcess::spaceArgsTest_data()
{
    QTest::addColumn<QStringList>("args");
    QTest::addColumn<QString>("stringArgs");

    // arg1 | arg2
    QTest::newRow("arg1 arg2") << (QStringList() << QString::fromLatin1("arg1") << QString::fromLatin1("arg2"))
                               << QString::fromLatin1("arg1 arg2");
    // "arg1" | ar "g2
    QTest::newRow("\"\"\"\"arg1\"\"\"\" \"ar \"\"\"g2\"") << (QStringList() << QString::fromLatin1("\"arg1\"") << QString::fromLatin1("ar \"g2"))
                                                          << QString::fromLatin1("\"\"\"\"arg1\"\"\"\" \"ar \"\"\"g2\"");
    // ar g1 | a rg 2
    QTest::newRow("\"ar g1\" \"a rg 2\"") << (QStringList() << QString::fromLatin1("ar g1") << QString::fromLatin1("a rg 2"))
                                          << QString::fromLatin1("\"ar g1\" \"a rg 2\"");
    // -lar g1 | -l"ar g2"
    QTest::newRow("\"-lar g1\" \"-l\"\"\"ar g2\"\"\"\"") << (QStringList() << QString::fromLatin1("-lar g1") << QString::fromLatin1("-l\"ar g2\""))
                                                         << QString::fromLatin1("\"-lar g1\" \"-l\"\"\"ar g2\"\"\"\"");
    // ar"g1
    QTest::newRow("ar\"\"\"\"g1") << (QStringList() << QString::fromLatin1("ar\"g1"))
                                  << QString::fromLatin1("ar\"\"\"\"g1");
    // ar/g1
    QTest::newRow("ar\\g1") << (QStringList() << QString::fromLatin1("ar\\g1"))
                            << QString::fromLatin1("ar\\g1");
    // ar\g"1
    QTest::newRow("ar\\g\"\"\"\"1") << (QStringList() << QString::fromLatin1("ar\\g\"1"))
                                    << QString::fromLatin1("ar\\g\"\"\"\"1");
    // arg\"1
    QTest::newRow("arg\\\"\"\"1") << (QStringList() << QString::fromLatin1("arg\\\"1"))
                                  << QString::fromLatin1("arg\\\"\"\"1");
    // """"
    QTest::newRow("\"\"\"\"\"\"\"\"\"\"\"\"") << (QStringList() << QString::fromLatin1("\"\"\"\""))
                                              << QString::fromLatin1("\"\"\"\"\"\"\"\"\"\"\"\"");
    // """" | "" ""
    QTest::newRow("\"\"\"\"\"\"\"\"\"\"\"\" \"\"\"\"\"\"\" \"\"\"\"\"\"\"") << (QStringList() << QString::fromLatin1("\"\"\"\"") << QString::fromLatin1("\"\" \"\""))
                                                                            << QString::fromLatin1("\"\"\"\"\"\"\"\"\"\"\"\" \"\"\"\"\"\"\" \"\"\"\"\"\"\"");
    // ""  ""
    QTest::newRow("\"\"\"\"\"\"\" \"\" \"\"\"\"\"\"\" (bogus double quotes)") << (QStringList() << QString::fromLatin1("\"\"  \"\""))
                                                                              << QString::fromLatin1("\"\"\"\"\"\"\" \"\" \"\"\"\"\"\"\"");
    // ""  ""
    QTest::newRow(" \"\"\"\"\"\"\" \"\" \"\"\"\"\"\"\"   (bogus double quotes)") << (QStringList() << QString::fromLatin1("\"\"  \"\""))
                                                                                 << QString::fromLatin1(" \"\"\"\"\"\"\" \"\" \"\"\"\"\"\"\"   ");
}

static QByteArray startFailMessage(const QString &program, const QProcess &process)
{
    QByteArray result = "Process '";
    result += program.toLocal8Bit();
    result += "' failed to start: ";
    result += process.errorString().toLocal8Bit();
    return result;
}

void tst_QProcess::spaceArgsTest()
{
    QFETCH(QStringList, args);
    QFETCH(QString, stringArgs);

    auto splitString = QProcess::splitCommand(stringArgs);
    QCOMPARE(args, splitString);

    QStringList programs;
    programs << QString::fromLatin1("testProcessSpacesArgs/nospace")
             << QString::fromLatin1("testProcessSpacesArgs/one space")
             << QString::fromLatin1("testProcessSpacesArgs/two space s");

    QProcess process;

    for (int i = 0; i < programs.size(); ++i) {
        QString program = programs.at(i);
        process.start(program, args);

        QByteArray errorMessage;
        bool started = process.waitForStarted();
        if (!started)
            errorMessage = startFailMessage(program, process);
        QVERIFY2(started, errorMessage.constData());
        QVERIFY(process.waitForFinished());
        QCOMPARE(process.exitStatus(), QProcess::NormalExit);
        QCOMPARE(process.exitCode(), 0);

        QStringList actual = QString::fromLatin1(process.readAll()).split("|");
        QVERIFY(!actual.isEmpty());
        // not interested in the program name, it might be different.
        actual.removeFirst();

        QCOMPARE(actual, args);
    }
}

#if defined(Q_OS_WIN)

void tst_QProcess::nativeArguments()
{
    QProcess proc;

    // This doesn't actually need special quoting, so it is pointless to use
    // native arguments here, but that's not the point of this test.
    proc.setNativeArguments("hello kitty, \"*\"!");

    proc.start(QString::fromLatin1("testProcessSpacesArgs/nospace"), QStringList());

    QVERIFY2(proc.waitForStarted(), qPrintable(proc.errorString()));
    QVERIFY(proc.waitForFinished());
    QCOMPARE(proc.exitStatus(), QProcess::NormalExit);
    QCOMPARE(proc.exitCode(), 0);

    QStringList actual = QString::fromLatin1(proc.readAll()).split(QLatin1Char('|'));
    QVERIFY(!actual.isEmpty());
    // not interested in the program name, it might be different.
    actual.removeFirst();
    QStringList expected;
    expected << "hello" << "kitty," << "*!";
    QCOMPARE(actual, expected);
}

void tst_QProcess::createProcessArgumentsModifier()
{
    int calls = 0;
    const QString reversedCommand = "lamroNssecorPtset/lamroNssecorPtset";
    QProcess process;
    process.setCreateProcessArgumentsModifier([&calls] (QProcess::CreateProcessArguments *args)
    {
        calls++;
        std::reverse(args->arguments, args->arguments + wcslen(args->arguments) - 1);
    });
    process.start(reversedCommand);
    QVERIFY2(process.waitForStarted(), qUtf8Printable(process.errorString()));
    QVERIFY(process.waitForFinished());
    QCOMPARE(calls, 1);

    process.setCreateProcessArgumentsModifier(QProcess::CreateProcessArgumentModifier());
    QVERIFY(!process.waitForStarted());
    QCOMPARE(calls, 1);
}
#endif // Q_OS_WIN

#ifdef Q_OS_UNIX
static constexpr int sigs[] = { SIGABRT, SIGILL, SIGSEGV };
struct DisableCrashLogger
{
#if defined(RLIMIT_CORE)
    // disable core dumps too
    struct NoCoreDumps {
        struct rlimit rlim;
        NoCoreDumps()
        {
            if (getrlimit(RLIMIT_CORE, &rlim) == 0 && rlim.rlim_cur != 0) {
                struct rlimit newrlim = rlim;
                newrlim.rlim_cur = 0;
                setrlimit(RLIMIT_CORE, &newrlim);
            }
        }
        ~NoCoreDumps()
        {
            setrlimit(RLIMIT_CORE, &rlim);
        }
    } disableCoreDumps;
#endif // RLIMIT_CORE

    std::array<struct sigaction, std::size(sigs)> oldhandlers;
    DisableCrashLogger()
    {
        struct sigaction def = {};
        def.sa_handler = SIG_DFL;
        for (uint i = 0; i < std::size(sigs); ++i)
            sigaction(sigs[i], &def, &oldhandlers[i]);
    }
    ~DisableCrashLogger()
    {
        // restore them
        for (uint i = 0; i < std::size(sigs); ++i)
            sigaction(sigs[i], &oldhandlers[i], nullptr);
    }
};

QT_BEGIN_NAMESPACE
Q_AUTOTEST_EXPORT bool _qprocessUsingVfork() noexcept;
QT_END_NAMESPACE
static constexpr char messageFromChildProcess[] = "Message from the child process";
static_assert(std::char_traits<char>::length(messageFromChildProcess) <= PIPE_BUF);
static void childProcessModifier(int fd)
{
    QT_WRITE(fd, messageFromChildProcess, strlen(messageFromChildProcess));
    QT_CLOSE(fd);
}

void tst_QProcess::setChildProcessModifier_data()
{
    QTest::addColumn<bool>("detached");
    QTest::addColumn<bool>("useVfork");
    QTest::newRow("normal") << false << false;
    QTest::newRow("detached") << true << false;

#ifdef QT_BUILD_INTERNAL
    if (_qprocessUsingVfork()) {
        QTest::newRow("normal-vfork") << false << true;
        QTest::newRow("detached-vfork") << true << true;
    }
#endif
}

void tst_QProcess::setChildProcessModifier()
{
    QFETCH(bool, detached);
    QFETCH(bool, useVfork);
    int pipes[2] = { -1 , -1 };
    QVERIFY(qt_safe_pipe(pipes) == 0);

    QProcess process;
    if (useVfork)
        process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork);
    process.setChildProcessModifier([pipes]() {
        ::childProcessModifier(pipes[1]);
    });
    process.setProgram("testProcessNormal/testProcessNormal");
    if (detached) {
        process.startDetached();
    } else {
        process.start("testProcessNormal/testProcessNormal");
        if (process.state() != QProcess::Starting)
            QCOMPARE(process.state(), QProcess::Running);
        QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
        QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString()));
        QCOMPARE(process.exitStatus(), QProcess::NormalExit);
        QCOMPARE(process.exitCode(), 0);
    }

    char buf[sizeof messageFromChildProcess] = {};
    qt_safe_close(pipes[1]);
    QCOMPARE(qt_safe_read(pipes[0], buf, sizeof(buf)), qint64(sizeof(messageFromChildProcess)) - 1);
    QCOMPARE(buf, messageFromChildProcess);
    qt_safe_close(pipes[0]);
}

void tst_QProcess::failChildProcessModifier()
{
    static const char failureMsg[] =
            "Some error message from the child process would go here if this were a "
            "real application";
    static_assert(sizeof(failureMsg) < _POSIX_PIPE_BUF / 2,
            "Implementation detail: the length of the message is limited");

    QFETCH(bool, detached);
    QFETCH(bool, useVfork);

    QProcess process;
    if (useVfork)
        process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork);
    process.setChildProcessModifier([&process]() {
        process.failChildProcessModifier(failureMsg, EPERM);
    });
    process.setProgram("testProcessNormal/testProcessNormal");

    if (detached) {
        qint64 pid;
        QVERIFY(!process.startDetached(&pid));
        QCOMPARE(pid, -1);
    } else {
        process.start();
        QVERIFY(!process.waitForStarted(5000));
    }

    QString errMsg = process.errorString();
    QVERIFY2(errMsg.startsWith("Child process modifier reported error: "_L1 + failureMsg),
             qPrintable(errMsg));
    QVERIFY2(errMsg.endsWith(strerror(EPERM)), qPrintable(errMsg));
}

void tst_QProcess::throwInChildProcessModifier()
{
#ifndef __cpp_exceptions
    Q_SKIP("Exceptions disabled.");
#else
    static constexpr char What[] = "tst_QProcess::throwInChildProcessModifier()::MyException";
    struct MyException : std::exception {
        const char *what() const noexcept override { return What; }
    };
    QProcess process;
    process.setChildProcessModifier([]() {
        throw MyException();
    });
    process.setProgram("testProcessNormal/testProcessNormal");

    process.start();
    QVERIFY(!process.waitForStarted(5000));
    QCOMPARE(process.state(), QProcess::NotRunning);
    QCOMPARE(process.error(), QProcess::FailedToStart);
    QVERIFY2(process.errorString().contains("Child process modifier threw an exception"),
             qPrintable(process.errorString()));
    QVERIFY2(process.errorString().contains(What),
             qPrintable(process.errorString()));

    // try again, to ensure QProcess internal state wasn't corrupted
    process.start();
    QVERIFY(!process.waitForStarted(5000));
    QCOMPARE(process.state(), QProcess::NotRunning);
    QCOMPARE(process.error(), QProcess::FailedToStart);
    QVERIFY2(process.errorString().contains("Child process modifier threw an exception"),
             qPrintable(process.errorString()));
    QVERIFY2(process.errorString().contains(What),
             qPrintable(process.errorString()));
#endif
}

void tst_QProcess::terminateInChildProcessModifier_data()
{
    using F = std::function<void(void)>;
    QTest::addColumn<F>("function");
    QTest::addColumn<QProcess::ExitStatus>("exitStatus");
    QTest::addColumn<bool>("stderrIsEmpty");

    QTest::newRow("_exit") << F([]() { _exit(0); }) << QProcess::NormalExit << true;
    QTest::newRow("abort") << F(std::abort) << QProcess::CrashExit << true;
    QTest::newRow("sigkill") << F([]() { raise(SIGKILL); }) << QProcess::CrashExit << true;
    QTest::newRow("terminate") << F(std::terminate) << QProcess::CrashExit
                               << (std::get_terminate() == std::abort);
    QTest::newRow("crash") << F([]() { tst_QProcessCrash::crash(); }) << QProcess::CrashExit << true;
}

void tst_QProcess::terminateInChildProcessModifier()
{
    QFETCH(std::function<void(void)>, function);
    QFETCH(QProcess::ExitStatus, exitStatus);
    QFETCH(bool, stderrIsEmpty);

    // testForwardingHelper prints to both stdout and stderr, so if we fail to
    // fail we should be able to tell too
    QProcess process;
    process.setChildProcessModifier(function);
    process.setProgram("testForwardingHelper/testForwardingHelper");
    process.setArguments({ "/dev/null" });

    // temporarily disable QTest's crash logger while starting the child process
    {
        DisableCrashLogger d;
        process.start();
    }

    QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString()));
    QCOMPARE(process.exitStatus(), exitStatus);
    QCOMPARE(process.readAllStandardOutput(), QByteArray());

    // some environments print extra stuff to stderr when we crash

#if defined(Q_OS_QNX)
    return;
#elif defined(Q_OS_MACOS)
    if (QOperatingSystemVersion::current() >= QOperatingSystemVersion::MacOSSequoia) {
        if (qEnvironmentVariableIsSet("SWIFT_BACKTRACE"))
            return; // Swift's crash reporting is printed to stderr
    }
#else
    if (QTestPrivate::isRunningArmOnX86())
        return;
#endif

    QByteArray standardError = process.readAllStandardError();
    QVERIFY2(standardError.isEmpty() == stderrIsEmpty,
             "stderr was: " + standardError);
}

void tst_QProcess::raiseInChildProcessModifier()
{
#ifdef QT_BUILD_INTERNAL
    // This is similar to the above, but knowing that raise() doesn't unblock
    // signals, unlike abort(), this implies that
    //  1) the raise() in the child modifier will not run our handler
    //  2) the write() to stdout after that will run
    //  3) QProcess resets the signal handlers to the defaults, then unblocks
    //  4) at that point, the signal will be delivered to the child, but our
    //     handler is no longer active so there'll be no write() to stderr
    //
    // Note for maintenance: if in the future this test causes the parent
    // process to die with SIGUSR1, it means the C library is buggy and is
    // using a cached PID in the child process after vfork().
    if (!QT_PREPEND_NAMESPACE(_qprocessUsingVfork()))
        QSKIP("QProcess will only block Unix signals when using vfork()");

    // we use SIGUSR1 because QtTest doesn't log it and because its default
    // action is termination, not core dumping
    struct SigUsr1Handler {
        SigUsr1Handler()
        {
            struct sigaction sa = {};
            sa.sa_flags = SA_RESETHAND;
            sa.sa_handler = [](int) {
                static const char msg[] = "SIGUSR1 handler was run";
                (void)write(STDERR_FILENO, msg, strlen(msg));
                raise(SIGUSR1);     // re-raise
            };
            sigaction(SIGUSR1, &sa, nullptr);
        }
        ~SigUsr1Handler() { restore(); }
        static void restore() { signal(SIGUSR1, SIG_DFL); }
    } sigUsr1Handler;

    QProcess process;

    // QProcess will block signals with UseVFork
    process.setUnixProcessParameters(QProcess::UnixProcessFlag::UseVFork |
                                     QProcess::UnixProcessFlag::ResetSignalHandlers);
    process.setChildProcessModifier([]() {
        raise(SIGUSR1);
        ::childProcessModifier(STDOUT_FILENO);
    });

    // testForwardingHelper prints to both stdout and stderr, so if we fail to
    // fail we should be able to tell too
    process.setProgram("testForwardingHelper/testForwardingHelper");
    process.setArguments({ "/dev/null" });

    process.start();
    QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    QVERIFY2(process.waitForFinished(5000), qPrintable(process.errorString()));
    QCOMPARE(process.error(), QProcess::Crashed);

    // ensure the write() from the child modifier DID get run
    QCOMPARE(process.readAllStandardOutput(), messageFromChildProcess);

    // some environments print extra stuff to stderr when we crash
    if (!QTestPrivate::isRunningArmOnX86()) {
        // and write() from the SIGUSR1 handler did not
        QCOMPARE(process.readAllStandardError(), QByteArray());
    }
#else
    QSKIP("Requires QT_BUILD_INTERNAL symbols");
#endif
}

void tst_QProcess::unixProcessParameters_data()
{
    QTest::addColumn<QProcess::UnixProcessParameters>("params");
    QTest::addColumn<QString>("cmd");
    QTest::newRow("defaults") << QProcess::UnixProcessParameters{} << QString();

    auto addRow = [](const char *cmd, QProcess::UnixProcessFlags flags) {
        QProcess::UnixProcessParameters params = {};
        params.flags = flags;
        QTest::addRow("%s", cmd) << params << cmd;
    };
    using P = QProcess::UnixProcessFlag;
    addRow("reset-sighand", P::ResetSignalHandlers);
    addRow("ignore-sigpipe", P::IgnoreSigPipe);
    addRow("file-descriptors", P::CloseFileDescriptors);
    addRow("setsid", P::CreateNewSession);
    addRow("reset-ids", P::ResetIds);
    addRow("no-coredumps", P::DisableCoreDumps);

    // On FreeBSD, we need to be session leader to disconnect from the CTTY
    addRow("noctty", P::DisconnectControllingTerminal | P::CreateNewSession);
}

void tst_QProcess::unixProcessParameters()
{
    QFETCH(QProcess::UnixProcessParameters, params);
    QFETCH(QString, cmd);

    // set up a few things
    struct Scope {
        int devnull;
        struct sigaction old_sigusr1, old_sigpipe;
        struct rlimit old_corelimit = {};
        Scope()
        {
            int fd = open("/dev/null", O_RDONLY);
            devnull = fcntl(fd, F_DUPFD, 100);
            close(fd);

            // we ignore SIGUSR1 and reset SIGPIPE to Terminate
            struct sigaction act = {};
            sigemptyset(&act.sa_mask);
            act.sa_handler = SIG_IGN;
            sigaction(SIGUSR1, &act, &old_sigusr1);
            act.sa_handler = SIG_DFL;
            sigaction(SIGPIPE, &act, &old_sigpipe);

            // and we block SIGUSR2
            sigset_t *set = &act.sa_mask;               // reuse this sigset_t
            sigaddset(set, SIGUSR2);
            sigprocmask(SIG_BLOCK, set, nullptr);

            if (getrlimit(RLIMIT_CORE, &old_corelimit) == 0 && old_corelimit.rlim_max) {
                struct rlimit new_corelimit = old_corelimit;
                new_corelimit.rlim_cur = new_corelimit.rlim_max;
                if (setrlimit(RLIMIT_CORE, &new_corelimit) != 0)
                    old_corelimit = {};
            }
        }
        ~Scope()
        {
            if (devnull != -1)
                dismiss();
        }
        void dismiss()
        {
            close(devnull);
            sigaction(SIGUSR1, &old_sigusr1, nullptr);
            sigaction(SIGPIPE, &old_sigpipe, nullptr);
            devnull = -1;

            sigset_t *set = &old_sigusr1.sa_mask;       // reuse this sigset_t
            sigaddset(set, SIGUSR2);
            sigprocmask(SIG_BLOCK, set, nullptr);

            if (old_corelimit.rlim_max)
                setrlimit(RLIMIT_CORE, &old_corelimit);
        }
    } scope;

    if (params.flags & QProcess::UnixProcessFlag::ResetIds) {
        if (getuid() == geteuid() && getgid() == getegid())
            qInfo("Process has identical real and effective IDs; this test will do nothing");
    }

    if (params.flags & QProcess::UnixProcessFlag::DisconnectControllingTerminal) {
        if (int fd = open("/dev/tty", O_RDONLY); fd < 0) {
            qInfo("Process has no controlling terminal; this test will do nothing");
            close(fd);
        }
    }

    if (params.flags & QProcess::UnixProcessFlag::DisableCoreDumps
            && scope.old_corelimit.rlim_max == 0) {
        QSKIP("Cannot raise the core size limit (hard limit is set to zero)");
    }

    QProcess process;
    process.setUnixProcessParameters(params);
    process.setStandardInputFile(QProcess::nullDevice());   // so we can't mess with SIGPIPE
    process.setProgram("testUnixProcessParameters/testUnixProcessParameters");
    process.setArguments({ cmd, QString::number(scope.devnull) });
    process.start();
    QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    QVERIFY(process.waitForFinished(5000));

    const QString stdErr = process.readAllStandardError();
    QCOMPARE(stdErr, QString());
    QCOMPARE(process.readAll(), QString());
    QCOMPARE(process.exitCode(), 0);
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
}

void tst_QProcess::impossibleUnixProcessParameters_data()
{
    using P = QProcess::UnixProcessParameters;
    QTest::addColumn<P>("params");
    QTest::newRow("setsid") << P{ QProcess::UnixProcessFlag::CreateNewSession };
}

void tst_QProcess::impossibleUnixProcessParameters()
{
    QFETCH(QProcess::UnixProcessParameters, params);

    QProcess process;
    if (params.flags & QProcess::UnixProcessFlag::CreateNewSession) {
        process.setChildProcessModifier([]() {
            // double setsid() should cause the second to fail
            setsid();
        });
    }
    process.setUnixProcessParameters(params);
    process.start("testProcessNormal/testProcessNormal");

    QVERIFY(!process.waitForStarted(5000));
    qDebug() << process.errorString();
}

void tst_QProcess::unixProcessParametersAndChildModifier()
{
    static constexpr char message[] = "Message from the handler function\n";
    static_assert(std::char_traits<char>::length(message) <= PIPE_BUF);
    QProcess process;
    QAtomicInt vforkControl;
    int pipes[2];

    pid_t oldpgid = getpgrp();

    QVERIFY2(pipe(pipes) == 0, qPrintable(qt_error_string()));
    auto pipeGuard0 = qScopeGuard([=] { close(pipes[0]); });
    {
        auto pipeGuard1 = qScopeGuard([=] { close(pipes[1]); });

        // verify that our modifier runs before the parameters are applied
        process.setChildProcessModifier([=, &vforkControl] {
            const char *pgidmsg = "PGID mismatch. ";
            if (getpgrp() != oldpgid)
                (void)write(pipes[1], pgidmsg, strlen(pgidmsg));
            (void)write(pipes[1], message, strlen(message));
            vforkControl.storeRelaxed(1);
        });
        auto flags = QProcess::UnixProcessFlag::CloseFileDescriptors |
                QProcess::UnixProcessFlag::CreateNewSession |
                QProcess::UnixProcessFlag::UseVFork;
        process.setUnixProcessParameters({ flags });
        process.setProgram("testUnixProcessParameters/testUnixProcessParameters");
        process.setArguments({ "file-descriptors", QString::number(pipes[1]) });
        process.start();
        QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    } // closes the writing end of the pipe

    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.readAllStandardError(), QString());
    QCOMPARE(process.readAll(), QString());

    char buf[2 * sizeof(message)];
    int r = read(pipes[0], buf, sizeof(buf));
    QVERIFY2(r >= 0, qPrintable(qt_error_string()));
    QCOMPARE(QByteArrayView(buf, r), message);

    if (haveWorkingVFork)
        QVERIFY2(vforkControl.loadRelaxed(), "QProcess doesn't appear to have used vfork()");
}

void tst_QProcess::unixProcessParametersOtherFileDescriptors()
{
    constexpr int TargetFileDescriptor = 3;
    int fd1 = open("/dev/null", O_RDONLY);
    int devnull = fcntl(fd1, F_DUPFD, 100); // instead of F_DUPFD_CLOEXEC
    close(fd1);

    auto closeFds = qScopeGuard([&] {
        close(devnull);
    });

    QProcess process;
    QProcess::UnixProcessParameters params;
    params.flags = QProcess::UnixProcessFlag::CloseFileDescriptors
            | QProcess::UnixProcessFlag::UseVFork;
    params.lowestFileDescriptorToClose = 4;
    process.setUnixProcessParameters(params);
    process.setChildProcessModifier([devnull, &process]() {
        if (dup2(devnull, TargetFileDescriptor) != TargetFileDescriptor)
            process.failChildProcessModifier("dup2", errno);
    });
    process.setProgram("testUnixProcessParameters/testUnixProcessParameters");
    process.setArguments({ "file-descriptors2", QString::number(TargetFileDescriptor),
                           QString::number(devnull) });
    process.start();

    QVERIFY2(process.waitForStarted(5000), qPrintable(process.errorString()));
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.readAllStandardError(), QString());
    QCOMPARE(process.readAll(), QString());
    QCOMPARE(process.exitCode(), 0);
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
}
#endif

void tst_QProcess::exitCodeTest()
{
    for (int i = 0; i < 255; ++i) {
        QProcess process;
        process.start("testExitCodes/testExitCodes", {QString::number(i)});
        QVERIFY(process.waitForFinished(5000));
        QCOMPARE(process.exitCode(), i);
        QCOMPARE(process.error(), QProcess::UnknownError);
    }
}

void tst_QProcess::failToStart()
{
    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");
    qRegisterMetaType<QProcess::ProcessState>("QProcess::ProcessState");

    QProcess process;
    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorSpy(&process, &QProcess::errorOccurred);
    QSignalSpy finishedSpy(&process, &QProcess::finished);
    QVERIFY(stateSpy.isValid());
    QVERIFY(errorSpy.isValid());
    QVERIFY(finishedSpy.isValid());

// OS X and HP-UX have a really low default process limit (~100), so spawning
// to many processes here will cause test failures later on.
#if defined Q_OS_HPUX
   const int attempts = 15;
#elif defined Q_OS_DARWIN
   const int attempts = 15;
#else
   const int attempts = 50;
#endif

    for (int j = 0; j < 8; ++j) {
        for (int i = 0; i < attempts; ++i) {
            QCOMPARE(errorSpy.size(), j * attempts + i);
            process.start("/blurp");

            switch (j) {
            case 0:
            case 1:
                QVERIFY(!process.waitForStarted());
                break;
            case 2:
            case 3:
                QVERIFY(!process.waitForFinished());
                break;
            case 4:
            case 5:
                QVERIFY(!process.waitForReadyRead());
                break;
            case 6:
            case 7:
            default:
                QVERIFY(!process.waitForBytesWritten());
                break;
            }

            QCOMPARE(process.error(), QProcess::FailedToStart);
            QCOMPARE(errorSpy.size(), j * attempts + i + 1);
            QCOMPARE(finishedSpy.size(), 0);

            int it = j * attempts + i + 1;

            QCOMPARE(stateSpy.size(), it * 2);
            QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(it * 2 - 2).at(0)), QProcess::Starting);
            QCOMPARE(qvariant_cast<QProcess::ProcessState>(stateSpy.at(it * 2 - 1).at(0)), QProcess::NotRunning);
        }
    }
}

void tst_QProcess::failToStartWithWait()
{
    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");

    QProcess process;
    QEventLoop loop;
    QSignalSpy errorSpy(&process, &QProcess::errorOccurred);
    QSignalSpy finishedSpy(&process, &QProcess::finished);
    QVERIFY(errorSpy.isValid());
    QVERIFY(finishedSpy.isValid());

    for (int i = 0; i < 50; ++i) {
        process.start("/blurp", QStringList() << "-v" << "-debug");
        process.waitForStarted();

        QCOMPARE(process.error(), QProcess::FailedToStart);
        QCOMPARE(errorSpy.size(), i + 1);
        QCOMPARE(finishedSpy.size(), 0);
    }
}

void tst_QProcess::failToStartWithEventLoop()
{
    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");

    QProcess process;
    QEventLoop loop;
    QSignalSpy errorSpy(&process, &QProcess::errorOccurred);
    QSignalSpy finishedSpy(&process, &QProcess::finished);
    QVERIFY(errorSpy.isValid());
    QVERIFY(finishedSpy.isValid());

    // The error signal may be emitted before start() returns
    connect(&process, &QProcess::errorOccurred, &loop, &QEventLoop::quit, Qt::QueuedConnection);


    for (int i = 0; i < 50; ++i) {
        process.start("/blurp", QStringList() << "-v" << "-debug");

        loop.exec();

        QCOMPARE(process.error(), QProcess::FailedToStart);
        QCOMPARE(errorSpy.size(), i + 1);
        QCOMPARE(finishedSpy.size(), 0);
    }
}

void tst_QProcess::failToStartEmptyArgs_data()
{
    QTest::addColumn<int>("startOverload");
    QTest::newRow("start(QString, QStringList, OpenMode)") << 0;
    QTest::newRow("start(OpenMode)") << 1;
}

void tst_QProcess::failToStartEmptyArgs()
{
    QFETCH(int, startOverload);
    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");

    QProcess process;
    QSignalSpy errorSpy(&process, static_cast<QProcessErrorSignal>(&QProcess::errorOccurred));
    QVERIFY(errorSpy.isValid());

    switch (startOverload) {
    case 0:
        process.start(QString(), QStringList(), QIODevice::ReadWrite);
        break;
    case 1:
        process.start(QIODevice::ReadWrite);
        break;
    default:
        QFAIL("Unhandled QProcess::start overload.");
    };

    QVERIFY(!process.waitForStarted());
    QCOMPARE(errorSpy.size(), 1);
    QCOMPARE(process.error(), QProcess::FailedToStart);
}

void tst_QProcess::removeFileWhileProcessIsRunning()
{
    QFile file(m_temporaryDir.path() + QLatin1String("/removeFile.txt"));
    QVERIFY(file.open(QFile::WriteOnly));

    QProcess process;
    process.start("testProcessEcho/testProcessEcho");

    QVERIFY(process.waitForStarted(5000));

    QVERIFY(file.remove());

    process.write("", 1);
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::setEnvironment_data()
{
    QTest::addColumn<QString>("name");
    QTest::addColumn<QString>("value");

    QTest::newRow("setting-empty") << "tst_QProcess" << "";
    QTest::newRow("setting") << "tst_QProcess" << "value";

#ifdef Q_OS_WIN
    QTest::newRow("unsetting") << "PROMPT" << QString();
    QTest::newRow("overriding") << "PROMPT" << "value";
#else
    QTest::newRow("unsetting") << "PATH" << QString();
    QTest::newRow("overriding") << "PATH" << "value";
#endif
}

void tst_QProcess::setEnvironment()
{
    // make sure our environment variables are correct
    QVERIFY(qgetenv("tst_QProcess").isEmpty());
    QVERIFY(!qgetenv("PATH").isEmpty());
#ifdef Q_OS_WIN
    QVERIFY(!qgetenv("PROMPT").isEmpty());
#endif

    QFETCH(QString, name);
    QFETCH(QString, value);
    QString executable = QDir::currentPath() + "/testProcessEnvironment/testProcessEnvironment";

    {
        QProcess process;
        QStringList environment = QProcess::systemEnvironment();
        if (value.isNull()) {
            int pos;
            QRegularExpression rx(name + "=.*");
#ifdef Q_OS_WIN
            rx.setPatternOptions(QRegularExpression::CaseInsensitiveOption);
#endif
            while ((pos = environment.indexOf(rx)) != -1)
                environment.removeAt(pos);
        } else {
            environment.append(name + '=' + value);
        }
        process.setEnvironment(environment);
        process.start(executable, QStringList() << name);

        QVERIFY(process.waitForFinished());
        if (value.isNull())
            QCOMPARE(process.exitCode(), 1);
        else if (!value.isEmpty())
            QCOMPARE(process.exitCode(), 0);

        QCOMPARE(process.readAll(), value.toLocal8Bit());
    }

    // re-do the test but set the environment twice, to make sure
    // that the latter addition overrides
    // this test doesn't make sense in unsetting
    if (!value.isNull()) {
        QProcess process;
        QStringList environment = QProcess::systemEnvironment();
        environment.prepend(name + "=This is not the right value");
        environment.append(name + '=' + value);
        process.setEnvironment(environment);
        process.start(executable, QStringList() << name);

        QVERIFY(process.waitForFinished());
        if (!value.isEmpty())
            QCOMPARE(process.exitCode(), 0);

        QCOMPARE(process.readAll(), value.toLocal8Bit());
    }
}

void tst_QProcess::setProcessEnvironment_data()
{
    setEnvironment_data();
}

void tst_QProcess::setProcessEnvironment()
{
    // make sure our environment variables are correct
    QVERIFY(qgetenv("tst_QProcess").isEmpty());
    QVERIFY(!qgetenv("PATH").isEmpty());
#ifdef Q_OS_WIN
    QVERIFY(!qgetenv("PROMPT").isEmpty());
#endif

    QFETCH(QString, name);
    QFETCH(QString, value);
    QString executable = QDir::currentPath() + "/testProcessEnvironment/testProcessEnvironment";

    {
        QProcess process;
        QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
        if (value.isNull())
            environment.remove(name);
        else
            environment.insert(name, value);
        process.setProcessEnvironment(environment);
        process.start(executable, QStringList() << name);

        QVERIFY(process.waitForFinished());
        if (value.isNull())
            QCOMPARE(process.exitCode(), 1);
        else if (!value.isEmpty())
            QCOMPARE(process.exitCode(), 0);

        QCOMPARE(process.readAll(), value.toLocal8Bit());
    }
}

void tst_QProcess::environmentIsSorted()
{
    QProcessEnvironment env;
    env.insert(QLatin1String("a"), QLatin1String("foo_a"));
    env.insert(QLatin1String("B"), QLatin1String("foo_B"));
    env.insert(QLatin1String("c"), QLatin1String("foo_c"));
    env.insert(QLatin1String("D"), QLatin1String("foo_D"));
    env.insert(QLatin1String("e"), QLatin1String("foo_e"));
    env.insert(QLatin1String("F"), QLatin1String("foo_F"));
    env.insert(QLatin1String("Path"), QLatin1String("foo_Path"));
    env.insert(QLatin1String("SystemRoot"), QLatin1String("foo_SystemRoot"));

    const QStringList envlist = env.toStringList();

#ifdef Q_OS_WIN32
    // The environment block passed to CreateProcess "[Requires that] All strings in the
    // environment block must be sorted alphabetically by name. The sort is case-insensitive,
    // Unicode order, without regard to locale."
    // https://msdn.microsoft.com/en-us/library/windows/desktop/ms682009(v=vs.85).aspx
    // So on Windows we sort that way.
    const QStringList expected = { QLatin1String("a=foo_a"),
                                   QLatin1String("B=foo_B"),
                                   QLatin1String("c=foo_c"),
                                   QLatin1String("D=foo_D"),
                                   QLatin1String("e=foo_e"),
                                   QLatin1String("F=foo_F"),
                                   QLatin1String("Path=foo_Path"),
                                   QLatin1String("SystemRoot=foo_SystemRoot") };
#else
    const QStringList expected = { QLatin1String("B=foo_B"),
                                   QLatin1String("D=foo_D"),
                                   QLatin1String("F=foo_F"),
                                   QLatin1String("Path=foo_Path"),
                                   QLatin1String("SystemRoot=foo_SystemRoot"),
                                   QLatin1String("a=foo_a"),
                                   QLatin1String("c=foo_c"),
                                   QLatin1String("e=foo_e") };
#endif
    QCOMPARE(envlist, expected);
}

void tst_QProcess::systemEnvironment()
{
    QVERIFY(!QProcess::systemEnvironment().isEmpty());
    QVERIFY(!QProcessEnvironment::systemEnvironment().isEmpty());

    QVERIFY(QProcessEnvironment::systemEnvironment().contains("PATH"));
    QVERIFY(!QProcess::systemEnvironment().filter(QRegularExpression("^PATH=", QRegularExpression::CaseInsensitiveOption)).isEmpty());
}

void tst_QProcess::spaceInName()
{
    QProcess process;
    process.start("test Space In Name/testSpaceInName", QStringList());
    QVERIFY(process.waitForStarted());
    process.write("", 1);
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

void tst_QProcess::lockupsInStartDetached()
{
    // Check that QProcess doesn't cause a lock up at this program's
    // exit if a thread was started and we tried to run a program that
    // doesn't exist. Before Qt 4.2, this used to lock up on Unix due
    // to calling ::exit instead of ::_exit if execve failed.

    QObject *dummy = new QObject(this);
    QHostInfo::lookupHost(QString("something.invalid"), dummy, SLOT(deleteLater()));
    QProcess::execute("yjhbrty");
    QProcess::startDetached("yjhbrty");
}

void tst_QProcess::atEnd2()
{
    QProcess process;

    process.start("testProcessEcho/testProcessEcho");
    process.write("Foo\nBar\nBaz\nBodukon\nHadukan\nTorwukan\nend\n");
    process.putChar('\0');
    QVERIFY(process.waitForFinished());
    QList<QByteArray> lines;
    while (!process.atEnd()) {
        lines << process.readLine();
    }
    QCOMPARE(lines.size(), 7);
}

void tst_QProcess::waitForReadyReadForNonexistantProcess()
{
    // Start a program that doesn't exist, process events and then try to waitForReadyRead
    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");

    QProcess process;
    QSignalSpy errorSpy(&process, &QProcess::errorOccurred);
    QSignalSpy finishedSpy(&process, &QProcess::finished);
    QVERIFY(errorSpy.isValid());
    QVERIFY(finishedSpy.isValid());

    QVERIFY(!process.waitForReadyRead()); // used to crash
    process.start("doesntexist");
    QVERIFY(!process.waitForReadyRead());
    QCOMPARE(errorSpy.size(), 1);
    QCOMPARE(errorSpy.at(0).at(0).toInt(), 0);
    QCOMPARE(finishedSpy.size(), 0);
}

void tst_QProcess::setStandardInputFile()
{
    static const char data[] = "A bunch\1of\2data\3\4\5\6\7...";
    QProcess process;
    QFile file(m_temporaryDir.path() + QLatin1String("/data-sif"));

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    QVERIFY(file.open(QIODevice::WriteOnly));
    file.write(data, sizeof data);
    file.close();

    process.setStandardInputFile(file.fileName());
    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted());
    QCOMPARE(errorOccurredSpy.size(), 0);
    QCOMPARE(stateSpy.size(), 2);
    QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
    QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::Running);
    stateSpy.clear();

    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
        QByteArray all = process.readAll();
    QCOMPARE(all.size(), int(sizeof data) - 1); // testProcessEcho drops the ending \0
    QVERIFY(all == data);

    QProcess process2;
    process2.setStandardInputFile(QProcess::nullDevice());
    process2.start("testProcessEcho/testProcessEcho");
    QVERIFY(process2.waitForFinished());
    all = process2.readAll();
    QCOMPARE(all.size(), 0);
}

void tst_QProcess::setStandardInputFileFailure()
{
    QProcess process;
    process.setStandardInputFile(nonExistentFileName);

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(!process.waitForStarted());

    QCOMPARE(errorOccurredSpy.size(), 1);
    QCOMPARE(errorOccurredSpy[0][0].value<QProcess::ProcessError>(), QProcess::FailedToStart);

    QCOMPARE(stateSpy.size(), 2);
    QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
    QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning);
}

void tst_QProcess::setStandardOutputFile_data()
{
    QTest::addColumn<QProcess::ProcessChannel>("channelToTest");
    QTest::addColumn<QProcess::ProcessChannelMode>("channelMode");
    QTest::addColumn<bool>("append");

    QTest::newRow("stdout-truncate") << QProcess::StandardOutput
                                     << QProcess::SeparateChannels
                                     << false;
    QTest::newRow("stdout-append") << QProcess::StandardOutput
                                   << QProcess::SeparateChannels
                                   << true;

    QTest::newRow("stderr-truncate") << QProcess::StandardError
                                     << QProcess::SeparateChannels
                                     << false;
    QTest::newRow("stderr-append") << QProcess::StandardError
                                   << QProcess::SeparateChannels
                                   << true;

    QTest::newRow("merged-truncate") << QProcess::StandardOutput
                                     << QProcess::MergedChannels
                                     << false;
    QTest::newRow("merged-append") << QProcess::StandardOutput
                                   << QProcess::MergedChannels
                                   << true;
}

void tst_QProcess::setStandardOutputFile()
{
    static const char data[] = "Original data. ";
    static const char testdata[] = "Test data.";

    QFETCH(QProcess::ProcessChannel, channelToTest);
    QFETCH(QProcess::ProcessChannelMode, channelMode);
    QFETCH(bool, append);

    QIODevice::OpenMode mode = append ? QIODevice::Append : QIODevice::Truncate;

    // create the destination file with data
    QFile file(m_temporaryDir.path() + QLatin1String("/data-stdof-") + QLatin1String(QTest::currentDataTag()));
    QVERIFY(file.open(QIODevice::WriteOnly));
    file.write(data, sizeof data - 1);
    file.close();

    // run the process
    QProcess process;
    process.setProcessChannelMode(channelMode);
    if (channelToTest == QProcess::StandardOutput)
        process.setStandardOutputFile(file.fileName(), mode);
    else
        process.setStandardErrorFile(file.fileName(), mode);

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    process.start("testProcessEcho2/testProcessEcho2");
    QVERIFY(process.waitForStarted());
    QCOMPARE(errorOccurredSpy.size(), 0);
    QCOMPARE(stateSpy.size(), 2);
    QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
    QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::Running);
    stateSpy.clear();

    process.write(testdata, sizeof testdata);
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    // open the file again and verify the data
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray all = file.readAll();
    file.close();

    int expectedsize = sizeof testdata - 1;
    if (mode == QIODevice::Append) {
        QVERIFY(all.startsWith(data));
        expectedsize += sizeof data - 1;
    }
    if (channelMode == QProcess::MergedChannels) {
        expectedsize += sizeof testdata - 1;
    } else {
        QVERIFY(all.endsWith(testdata));
    }

    QCOMPARE(all.size(), expectedsize);
}

void tst_QProcess::setStandardOutputFileFailure()
{
    QFETCH(QProcess::ProcessChannel, channelToTest);
    QFETCH(QProcess::ProcessChannelMode, channelMode);
    QFETCH(bool, append);

    QIODevice::OpenMode mode = append ? QIODevice::Append : QIODevice::Truncate;

    // run the process
    QProcess process;
    process.setProcessChannelMode(channelMode);
    if (channelToTest == QProcess::StandardOutput)
        process.setStandardOutputFile(nonExistentFileName, mode);
    else
        process.setStandardErrorFile(nonExistentFileName, mode);

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    process.start("testProcessEcho2/testProcessEcho2");
    QVERIFY(!process.waitForStarted());
    QCOMPARE(errorOccurredSpy.size(), 1);
    QCOMPARE(errorOccurredSpy[0][0].value<QProcess::ProcessError>(), QProcess::FailedToStart);
    QCOMPARE(stateSpy.size(), 2);
    QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
    QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning);
}

void tst_QProcess::setStandardOutputFileNullDevice()
{
    static const char testdata[] = "Test data.";

    QProcess process;
    process.setStandardOutputFile(QProcess::nullDevice());
    process.start("testProcessEcho2/testProcessEcho2");
    process.write(testdata, sizeof testdata);
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
    QCOMPARE(process.bytesAvailable(), Q_INT64_C(0));

    QVERIFY(!QFileInfo(QProcess::nullDevice()).isFile());
}

void tst_QProcess::setStandardOutputFileAndWaitForBytesWritten()
{
    static const char testdata[] = "Test data.";

    QFile file(m_temporaryDir.path() + QLatin1String("/data-stdofawfbw"));
    QProcess process;
    process.setStandardOutputFile(file.fileName());
    process.start("testProcessEcho2/testProcessEcho2");
    QVERIFY2(process.waitForStarted(), qPrintable(process.errorString()));
    process.write(testdata, sizeof testdata);
    process.waitForBytesWritten();
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    // open the file again and verify the data
    QVERIFY(file.open(QIODevice::ReadOnly));
    QByteArray all = file.readAll();
    file.close();

    QCOMPARE(all, QByteArray::fromRawData(testdata, sizeof testdata - 1));
}

void tst_QProcess::setStandardOutputProcess_data()
{
    QTest::addColumn<bool>("merged");
    QTest::addColumn<bool>("waitForBytesWritten");
    QTest::newRow("separate") << false << false;
    QTest::newRow("separate with waitForBytesWritten") << false << true;
    QTest::newRow("merged") << true << false;
}

void tst_QProcess::setStandardOutputProcess()
{
    QProcess source;
    QProcess intermediate;
    QProcess sink;

    QFETCH(bool, merged);
    QFETCH(bool, waitForBytesWritten);
    source.setProcessChannelMode(merged ? QProcess::MergedChannels : QProcess::SeparateChannels);
    source.setStandardOutputProcess(&intermediate);
    intermediate.setStandardOutputProcess(&sink);

    source.start("testProcessEcho2/testProcessEcho2");
    intermediate.setProgram("testProcessEcho/testProcessEcho");
    QVERIFY(intermediate.startDetached());
    sink.start("testProcessEcho2/testProcessEcho2");

    QByteArray data("Hello, World");
    source.write(data);
    if (waitForBytesWritten)
        source.waitForBytesWritten();
    source.closeWriteChannel();
    QVERIFY(source.waitForFinished());
    QCOMPARE(source.exitStatus(), QProcess::NormalExit);
    QCOMPARE(source.exitCode(), 0);
    QVERIFY(sink.waitForFinished());
    QCOMPARE(sink.exitStatus(), QProcess::NormalExit);
    QCOMPARE(sink.exitCode(), 0);
    QByteArray all = sink.readAll();

    if (!merged)
        QCOMPARE(all, data);
    else
        QCOMPARE(all, QByteArray("HHeelllloo,,  WWoorrlldd"));
}

void tst_QProcess::fileWriterProcess()
{
    const QByteArray line = QByteArrayLiteral(" -- testing testing 1 2 3\n");
    QByteArray stdinStr;
    stdinStr.reserve(5000 * (4 + line.size()) + 1);
    for (int i = 0; i < 5000; ++i) {
        stdinStr += QByteArray::number(i);
        stdinStr += line;
    }

    QElapsedTimer stopWatch;
    stopWatch.start();
    const QString fileName = m_temporaryDir.path() + QLatin1String("/fileWriterProcess.txt");
    const QString binary = QDir::currentPath() + QLatin1String("/fileWriterProcess/fileWriterProcess");

    do {
        if (QFile::exists(fileName))
            QVERIFY(QFile::remove(fileName));
        QProcess process;
        process.setWorkingDirectory(m_temporaryDir.path());
        process.start(binary, {}, QIODevice::ReadWrite | QIODevice::Text);
        process.write(stdinStr);
        process.closeWriteChannel();
        while (process.bytesToWrite()) {
            QVERIFY(stopWatch.elapsed() < 3500);
            QVERIFY(process.waitForBytesWritten(2000));
        }
        QVERIFY(process.waitForFinished());
        QCOMPARE(process.exitStatus(), QProcess::NormalExit);
        QCOMPARE(process.exitCode(), 0);
        QCOMPARE(QFile(fileName).size(), qint64(stdinStr.size()));
    } while (stopWatch.elapsed() < 3000);
}

void tst_QProcess::detachedProcessParameters_data()
{
    QTest::addColumn<QString>("outChannel");
    QTest::newRow("none") << QString();
    QTest::newRow("stdout") << QString("stdout");
    QTest::newRow("stderr") << QString("stderr");
}

void tst_QProcess::detachedProcessParameters()
{
    QFETCH(QString, outChannel);
    qint64 pid;

    QFile infoFile(m_temporaryDir.path() + QLatin1String("/detachedinfo.txt"));
    if (infoFile.exists())
        QVERIFY(infoFile.remove());
    QFile channelFile(m_temporaryDir.path() + QLatin1String("detachedinfo2.txt"));
    if (channelFile.exists())
        QVERIFY(channelFile.remove());

    QString workingDir = QDir::currentPath() + "/testDetached";

    QVERIFY(QFile::exists(workingDir));

    QVERIFY(qgetenv("tst_QProcess").isEmpty());
    QByteArray envVarValue("foobarbaz");
    QProcessEnvironment environment = QProcessEnvironment::systemEnvironment();
    environment.insert(QStringLiteral("tst_QProcess"), QString::fromUtf8(envVarValue));

    QProcess process;
    process.setProgram(QDir::currentPath() + QLatin1String("/testDetached/testDetached"));
#ifdef Q_OS_WIN
    int modifierCalls = 0;
    process.setCreateProcessArgumentsModifier(
        [&modifierCalls] (QProcess::CreateProcessArguments *) { modifierCalls++; });
#endif
    QStringList args(infoFile.fileName());
    if (!outChannel.isEmpty()) {
        args << QStringLiteral("--out-channel=") + outChannel;
        if (outChannel == "stdout")
            process.setStandardOutputFile(channelFile.fileName());
        else if (outChannel == "stderr")
            process.setStandardErrorFile(channelFile.fileName());
    }
    process.setArguments(args);
    process.setWorkingDirectory(workingDir);
    process.setProcessEnvironment(environment);
    QVERIFY(process.startDetached(&pid));

    QFileInfo fi(infoFile);
    fi.setCaching(false);
    //The guard counter ensures the test does not hang if the sub process fails.
    //Instead, the test will fail when trying to open & verify the sub process output file.
    for (int guard = 0; guard < 100 && fi.size() == 0; guard++) {
        QTest::qSleep(100);
    }

    QVERIFY(infoFile.open(QIODevice::ReadOnly | QIODevice::Text));
    QString actualWorkingDir = QString::fromUtf8(infoFile.readLine()).trimmed();
    QByteArray processIdString = infoFile.readLine().trimmed();
    QByteArray actualEnvVarValue = infoFile.readLine().trimmed();
    QByteArray infoFileContent;
    if (!outChannel.isEmpty()) {
        infoFile.seek(0);
        infoFileContent = infoFile.readAll();
    }
    infoFile.close();
    infoFile.remove();

    if (!outChannel.isEmpty()) {
        QVERIFY(channelFile.open(QIODevice::ReadOnly | QIODevice::Text));
        QByteArray channelContent = channelFile.readAll();
        channelFile.close();
        channelFile.remove();
        QCOMPARE(channelContent, infoFileContent);
    }

    bool ok = false;
    qint64 actualPid = processIdString.toLongLong(&ok);
    QVERIFY(ok);

    QCOMPARE(actualWorkingDir, workingDir);
    QCOMPARE(actualPid, pid);
    QCOMPARE(actualEnvVarValue, envVarValue);
#ifdef Q_OS_WIN
    QCOMPARE(modifierCalls, 1);
#endif
}

void tst_QProcess::switchReadChannels()
{
    const char data[] = "ABCD";

    QProcess process;

    process.start("testProcessEcho2/testProcessEcho2");
    process.write(data);
    process.closeWriteChannel();
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    for (int i = 0; i < 4; ++i) {
        process.setReadChannel(QProcess::StandardOutput);
        QCOMPARE(process.read(1), QByteArray(&data[i], 1));
        process.setReadChannel(QProcess::StandardError);
        QCOMPARE(process.read(1), QByteArray(&data[i], 1));
    }

    process.ungetChar('D');
    process.setReadChannel(QProcess::StandardOutput);
    process.ungetChar('D');
    process.setReadChannel(QProcess::StandardError);
    QCOMPARE(process.read(1), QByteArray("D"));
    process.setReadChannel(QProcess::StandardOutput);
    QCOMPARE(process.read(1), QByteArray("D"));
}

void tst_QProcess::discardUnwantedOutput()
{
    QProcess process;

    process.setProgram("testProcessEcho2/testProcessEcho2");
    process.start(QIODevice::WriteOnly);
    process.write("Hello, World");
    process.closeWriteChannel();
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    process.setReadChannel(QProcess::StandardOutput);
    QCOMPARE(process.bytesAvailable(), Q_INT64_C(0));
    process.setReadChannel(QProcess::StandardError);
    QCOMPARE(process.bytesAvailable(), Q_INT64_C(0));
}

// Q_OS_WIN - setWorkingDirectory will chdir before starting the process on unices
void tst_QProcess::setWorkingDirectory()
{
    QProcess process;
    process.setWorkingDirectory(m_temporaryDir.path());

    // use absolute path because on Windows, the executable is relative to the parent's CWD
    // while on Unix with fork it's relative to the child's (with posix_spawn, it could be either).
    process.start(QFileInfo("testSetWorkingDirectory/testSetWorkingDirectory").absoluteFilePath());

    QVERIFY2(process.waitForFinished(), process.errorString().toLocal8Bit());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    QByteArray workingDir = process.readAllStandardOutput();
    QCOMPARE(QDir(m_temporaryDir.path()).canonicalPath(), QDir(workingDir.constData()).canonicalPath());
}

void tst_QProcess::setNonExistentWorkingDirectory_data()
{
    simpleStartFail_data();
}

void tst_QProcess::setNonExistentWorkingDirectory()
{
    QFETCH(bool, detached);
    QProcess process;
    process.setWorkingDirectory(nonExistentFileName);

#ifdef Q_OS_UNIX
    QFETCH(bool, unixCloseFileDescriptors);
    if (unixCloseFileDescriptors)
        process.setUnixProcessParameters(QProcess::UnixProcessFlag::CloseFileDescriptors);
#endif

    QSignalSpy stateSpy(&process, &QProcess::stateChanged);
    QSignalSpy errorOccurredSpy(&process, &QProcess::errorOccurred);

    // use absolute path because on Windows, the executable is relative to the parent's CWD
    // while on Unix with fork it's relative to the child's (with posix_spawn, it could be either).
    process.setProgram(QFileInfo("testSetWorkingDirectory/testSetWorkingDirectory").absoluteFilePath());

    if (detached) {
        qint64 pid = -1;
        QVERIFY(!process.startDetached(&pid));
        QCOMPARE(pid, -1);
    } else {
        process.start();
        QVERIFY(!process.waitForFinished());

        QCOMPARE(stateSpy.size(), 2);
        QCOMPARE(stateSpy[0][0].value<QProcess::ProcessState>(), QProcess::Starting);
        QCOMPARE(stateSpy[1][0].value<QProcess::ProcessState>(), QProcess::NotRunning);
    }

    QCOMPARE(errorOccurredSpy.size(), 1);
    QCOMPARE(process.error(), QProcess::FailedToStart);
    QCOMPARE_NE(process.errorString(), "Unknown error");

#ifdef Q_OS_UNIX
    QVERIFY2(process.errorString().contains(": chdir: "), process.errorString().toLocal8Bit());
    int errorcode = ENOENT;
#else
    int errorcode = ERROR_DIRECTORY;
#endif
    QVERIFY2(process.errorString().contains(qt_error_string(errorcode)),
             process.errorString().toLocal8Bit());
}

void tst_QProcess::startFinishStartFinish()
{
    QProcess process;

    for (int i = 0; i < 3; ++i) {
        QCOMPARE(process.state(), QProcess::NotRunning);

        process.start("testProcessOutput/testProcessOutput");
        QVERIFY(process.waitForReadyRead(10000));
        QCOMPARE(QString::fromLatin1(process.readLine().trimmed()),
                 QString("0 -this is a number"));
        if (process.state() != QProcess::NotRunning) {
            QVERIFY(process.waitForFinished(10000));
            QCOMPARE(process.exitStatus(), QProcess::NormalExit);
            QCOMPARE(process.exitCode(), 0);
        }
    }
}

void tst_QProcess::invalidProgramString_data()
{
    QTest::addColumn<QString>("programString");
    QTest::newRow("null string") << QString();
    QTest::newRow("empty string") << QString("");
}

void tst_QProcess::invalidProgramString()
{
    QFETCH(QString, programString);
    QProcess process;

    qRegisterMetaType<QProcess::ProcessError>("QProcess::ProcessError");
    QSignalSpy spy(&process, &QProcess::errorOccurred);
    QVERIFY(spy.isValid());

    process.start(programString);
    QCOMPARE(process.error(), QProcess::FailedToStart);
    QCOMPARE(spy.size(), 1);

    QVERIFY(!QProcess::startDetached(programString));
}

void tst_QProcess::onlyOneStartedSignal()
{
    qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus");
    QProcess process;

    QSignalSpy spyStarted(&process,  &QProcess::started);
    QSignalSpy spyFinished(&process, &QProcess::finished);

    QVERIFY(spyStarted.isValid());
    QVERIFY(spyFinished.isValid());

    process.start("testProcessNormal/testProcessNormal");
    QVERIFY(process.waitForStarted(5000));
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(spyStarted.size(), 1);
    QCOMPARE(spyFinished.size(), 1);

    spyStarted.clear();
    spyFinished.clear();

    process.start("testProcessNormal/testProcessNormal");
    QVERIFY(process.waitForFinished(5000));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
    QCOMPARE(spyStarted.size(), 1);
    QCOMPARE(spyFinished.size(), 1);
}

class BlockOnReadStdOut : public QObject
{
    Q_OBJECT
public:
    BlockOnReadStdOut(QProcess *process)
    {
        connect(process, &QProcess::readyReadStandardOutput, this, &BlockOnReadStdOut::block);
    }

public slots:
    void block()
    {
        QThread::sleep(std::chrono::seconds{1});
    }
};

void tst_QProcess::finishProcessBeforeReadingDone()
{
    QProcess process;
    BlockOnReadStdOut blocker(&process);
    QEventLoop loop;
    connect(&process, &QProcess::finished, &loop, &QEventLoop::quit);
    process.start("testProcessOutput/testProcessOutput");
    QVERIFY(process.waitForStarted());
    loop.exec();
    QStringList lines = QString::fromLocal8Bit(process.readAllStandardOutput()).split(
            QRegularExpression(QStringLiteral("[\r\n]")), Qt::SkipEmptyParts);
    QVERIFY(!lines.isEmpty());
    QCOMPARE(lines.last(), QStringLiteral("10239 -this is a number"));
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

//-----------------------------------------------------------------------------
void tst_QProcess::waitForStartedWithoutStart()
{
    QProcess process;
    QVERIFY(!process.waitForStarted(5000));
}

//-----------------------------------------------------------------------------
void tst_QProcess::startStopStartStop()
{
    // we actually do start-stop x 3 :-)
    QProcess process;
    process.start("testProcessNormal/testProcessNormal");
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    process.start("testExitCodes/testExitCodes", QStringList() << "1");
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 1);

    process.start("testProcessNormal/testProcessNormal");
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);
}

//-----------------------------------------------------------------------------
void tst_QProcess::startStopStartStopBuffers_data()
{
    QTest::addColumn<QProcess::ProcessChannelMode>("channelMode1");
    QTest::addColumn<QProcess::ProcessChannelMode>("channelMode2");

    QTest::newRow("separate-separate") << QProcess::SeparateChannels << QProcess::SeparateChannels;
    QTest::newRow("separate-merged")   << QProcess::SeparateChannels << QProcess::MergedChannels;
    QTest::newRow("merged-separate")   << QProcess::MergedChannels   << QProcess::SeparateChannels;
    QTest::newRow("merged-merged")     << QProcess::MergedChannels   << QProcess::MergedChannels;
    QTest::newRow("merged-forwarded")  << QProcess::MergedChannels   << QProcess::ForwardedChannels;
}

void tst_QProcess::startStopStartStopBuffers()
{
    QFETCH(QProcess::ProcessChannelMode, channelMode1);
    QFETCH(QProcess::ProcessChannelMode, channelMode2);

    QProcess process;
    process.setProcessChannelMode(channelMode1);
    process.start("testProcessHang/testProcessHang");
    QVERIFY2(process.waitForReadyRead(), process.errorString().toLocal8Bit());
    if (channelMode1 == QProcess::SeparateChannels || channelMode1 == QProcess::ForwardedOutputChannel) {
        process.setReadChannel(QProcess::StandardError);
        if (process.bytesAvailable() == 0)
            QVERIFY(process.waitForReadyRead());
        process.setReadChannel(QProcess::StandardOutput);
    }

    // We want to test that the write buffer still has bytes after the child
    // exits. We can do that by writing data until the OS stops consuming data,
    // indicating that the pipe buffers are full. The initial value of 128 kB
    // should make this loop typicall run only once; the worst case I know of
    // is Linux, which defaults to 64 kB of buffer.

    QByteArray chunk(128 * 1024, 'a');
    do {
        process.write(chunk);
        QVERIFY(process.bytesToWrite() > 0);
        process.waitForBytesWritten(1);
    } while (process.bytesToWrite() == 0);
    chunk = {};
    process.kill();

    QVERIFY(process.waitForFinished());

#ifndef Q_OS_WIN
    // confirm that our buffers are still full
    // Note: this doesn't work on Windows because our buffers are drained into
    // QWindowsPipeWriter before being sent to the child process and are lost
    // in waitForFinished() -> processFinished() -> cleanup().
    QVERIFY(process.bytesToWrite() > 0);
    QVERIFY(process.bytesAvailable() > 0); // channelMode1 is not ForwardedChannels
    if (channelMode1 == QProcess::SeparateChannels || channelMode1 == QProcess::ForwardedOutputChannel) {
        process.setReadChannel(QProcess::StandardError);
        QVERIFY(process.bytesAvailable() > 0);
        process.setReadChannel(QProcess::StandardOutput);
    }
#endif

    process.setProcessChannelMode(channelMode2);
    process.start("testProcessEcho2/testProcessEcho2", {}, QIODevice::ReadWrite | QIODevice::Text);

    // the buffers should now be empty
    QCOMPARE(process.bytesToWrite(), qint64(0));
    QCOMPARE(process.bytesAvailable(), qint64(0));
    process.setReadChannel(QProcess::StandardError);
    QCOMPARE(process.bytesAvailable(), qint64(0));
    process.setReadChannel(QProcess::StandardOutput);

    process.write("line3\n");
    process.closeWriteChannel();
    QVERIFY(process.waitForFinished());
    QCOMPARE(process.exitStatus(), QProcess::NormalExit);
    QCOMPARE(process.exitCode(), 0);

    if (channelMode2 == QProcess::MergedChannels) {
        QCOMPARE(process.readAll(), QByteArray("lliinnee33\n\n"));
    } else if (channelMode2 != QProcess::ForwardedChannels) {
        QCOMPARE(process.readAllStandardOutput(), QByteArray("line3\n"));
        if (channelMode2 == QProcess::SeparateChannels)
            QCOMPARE(process.readAllStandardError(), QByteArray("line3\n"));
    }
}

void tst_QProcess::processEventsInAReadyReadSlot_data()
{
    QTest::addColumn<bool>("callWaitForReadyRead");

    QTest::newRow("no waitForReadyRead") << false;
    QTest::newRow("waitForReadyRead") << true;
}

void tst_QProcess::processEventsInAReadyReadSlot()
{
    // Test whether processing events in a readyReadXXX slot crashes. (QTBUG-48697)
    QFETCH(bool, callWaitForReadyRead);
    QProcess process;
    QObject::connect(&process, &QProcess::readyReadStandardOutput,
                     this, &tst_QProcess::processApplicationEvents);
    process.start("testProcessEcho/testProcessEcho");
    QVERIFY(process.waitForStarted());
    const QByteArray data(156, 'x');
    process.write(data.constData(), data.size() + 1);
    if (callWaitForReadyRead)
        QVERIFY(process.waitForReadyRead());
    if (process.state() == QProcess::Running)
        QVERIFY(process.waitForFinished());
}

enum class ChdirMode {
    None = 0,
    InParent,
    InChild
};
Q_DECLARE_METATYPE(ChdirMode)

void tst_QProcess::startFromCurrentWorkingDir_data()
{
    qRegisterMetaType<ChdirMode>();
    QTest::addColumn<QString>("programPrefix");
    QTest::addColumn<ChdirMode>("chdirMode");
    QTest::addColumn<bool>("success");

    constexpr bool IsWindows = true
#ifdef Q_OS_UNIX
            && false
#endif
            ;

    // baseline: trying to execute the directory, this can't possibly succeed!
    QTest::newRow("plain-same-cwd") << QString() << ChdirMode::None << false;

    // cross-platform behavior: neither OS searches the setWorkingDirectory()
    // dir without "./"
    QTest::newRow("plain-child-chdir") << QString() << ChdirMode::InChild << false;

    // cross-platform behavior: both OSes search the parent's CWD with "./"
    QTest::newRow("prefixed-parent-chdir") << "./" << ChdirMode::InParent << true;

    // opposite behaviors: Windows searches the parent's CWD and Unix searches
    // the child's with "./"
    QTest::newRow("prefixed-child-chdir") << "./" << ChdirMode::InChild << !IsWindows;

    // Windows searches the parent's CWD without "./"
    QTest::newRow("plain-parent-chdir") << QString() << ChdirMode::InParent << IsWindows;
}

void tst_QProcess::startFromCurrentWorkingDir()
{
    QFETCH(QString, programPrefix);
    QFETCH(ChdirMode, chdirMode);
    QFETCH(bool, success);

    QProcess process;
    qRegisterMetaType<QProcess::ProcessError>();
    QSignalSpy errorSpy(&process, &QProcess::errorOccurred);
    QVERIFY(errorSpy.isValid());

    // both the dir name and the executable name
    const QString target = QStringLiteral("testProcessNormal");
    process.setProgram(programPrefix + target);

#ifdef Q_OS_UNIX
    // Reset PATH, to be sure it doesn't contain . or the empty path.
    // We can't do this on Windows because DLLs are searched in PATH
    // and Windows always searches "." anyway.
    auto restoreEnv = qScopeGuard([old = qgetenv("PATH")] {
        qputenv("PATH", old);
    });
    qputenv("PATH", "/");
#endif

    switch (chdirMode) {
    case ChdirMode::InParent: {
        auto restoreCwd = qScopeGuard([old = QDir::currentPath()] {
            QDir::setCurrent(old);
        });
        QVERIFY(QDir::setCurrent(target));
        process.start();
        break;
    }
    case ChdirMode::InChild:
        process.setWorkingDirectory(target);
        Q_FALLTHROUGH();
    case ChdirMode::None:
        process.start();
        break;
    }

    QCOMPARE(process.waitForStarted(), success);
    QCOMPARE(errorSpy.size(), int(!success));
    if (success) {
        QVERIFY(process.waitForFinished());
    } else {
        QCOMPARE(process.error(), QProcess::FailedToStart);
    }
}

void tst_QProcess::syscallsAreRestartedInParent_data()
{
    QTest::addColumn<QString>("mode");
    QTest::newRow("normal") << "normal";
    QTest::newRow("detached") << "detached";
}

void tst_QProcess::syscallsAreRestartedInParent()
{
#ifdef Q_OS_WIN
    auto kill = [](qint64 pid, int sig) {
        HANDLE hnd = OpenProcess(PROCESS_TERMINATE, false, pid);
        TerminateProcess(hnd, sig);
        CloseHandle(hnd);
    };
#endif
#ifndef SIGTERM
    constexpr int SIGTERM = 1;      // doesn't matter
#endif

    // This is a test to ensure that an interruptible system call in the parent
    // process is restarted by the OS, instead of producing an EINTR error,
    // when a SIGCHLD gets delivered.
    // This applies to Unix systems, but it won't hurt doing it on Windows too.
    QFETCH(QString, mode);

    QProcess proc;
    auto stderrPrinter = qScopeGuard([&proc] {
        if (proc.state() == QProcess::Running) {
            proc.kill();
            proc.waitForFinished();
        }
        qWarning() << "stderr was:" << proc.readAllStandardError();
    });
    proc.setProgram("testProcessGrandchild/testProcessGrandchild");
    proc.setArguments({ mode, "testProcessEcho/testProcessEcho" });
    proc.start(QIODevice::ReadWrite | QIODevice::Text);
    QVERIFY(proc.waitForReadyRead());

    qint64 grandchildPid = proc.readAll().toLongLong(nullptr, 10);
    QCOMPARE_NE(grandchildPid, 0);

    // RACE CONDITION: we need the child to be in a system call
    constexpr std::chrono::milliseconds wait(100);
    QTest::qSleep(wait);
    kill(grandchildPid, SIGTERM);
    QTest::qSleep(wait);

    const char msg[] = "Content from parent\n";
    proc.write(msg, strlen(msg));
    proc.closeWriteChannel();
    QVERIFY(proc.waitForFinished());

#if defined(Q_OS_UNIX) && !defined(SA_RESTART)
    // Mandatory as of POSIX.1-2018 though
    QEXPECT_FAIL("normal", "Some OSes don't have SA_RESTART, so the system call gets interrupted", Continue);
#endif
    QByteArray all = proc.readAll();
    QVERIFY2(all.startsWith(QByteArray("from parent: ") + msg), all.constData());
    stderrPrinter.dismiss();
}

QTEST_MAIN(tst_QProcess)
#include "tst_qprocess.moc"
